CMM-2152: Fix Views card stat accuracy and halve its network calls#23101
Open
adalpari wants to merge 11 commits into
Open
CMM-2152: Fix Views card stat accuracy and halve its network calls#23101adalpari wants to merge 11 commits into
adalpari wants to merge 11 commits into
Conversation
Collaborator
Generated by 🚫 Danger |
Contributor
|
|
Contributor
|
|
Contributor
Project dependencies changeslist! Upgraded Dependencies
rs.wordpress.api:android:1434-93b8ac510865b086e6b3588c3e816b08a65d020c, (changed from 1434-e7e17e7168b28116f3c7975ac8e20757259589cb)
rs.wordpress.api:kotlin:1434-93b8ac510865b086e6b3588c3e816b08a65d020c, (changed from 1434-e7e17e7168b28116f3c7975ac8e20757259589cb)tree +--- project :libs:fluxc
-| \--- rs.wordpress.api:android:1434-e7e17e7168b28116f3c7975ac8e20757259589cb
-| +--- com.squareup.okhttp3:okhttp:5.4.0 (*)
-| +--- com.squareup.okhttp3:okhttp-tls:5.4.0
-| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
-| | +--- com.squareup.okio:okio:3.17.0 (*)
-| | \--- com.squareup.okhttp3:okhttp:5.4.0 (*)
-| +--- net.java.dev.jna:jna:5.19.1
-| +--- rs.wordpress.api:kotlin:1434-e7e17e7168b28116f3c7975ac8e20757259589cb
-| | +--- com.squareup.okhttp3:okhttp:5.4.0 (*)
-| | +--- com.squareup.okhttp3:okhttp-tls:5.4.0 (*)
-| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 -> 1.11.0 (*)
-| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
-| \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
+| \--- rs.wordpress.api:android:1434-93b8ac510865b086e6b3588c3e816b08a65d020c
+| +--- com.squareup.okhttp3:okhttp:5.4.0 (*)
+| +--- com.squareup.okhttp3:okhttp-tls:5.4.0
+| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
+| | +--- com.squareup.okio:okio:3.17.0 (*)
+| | \--- com.squareup.okhttp3:okhttp:5.4.0 (*)
+| +--- net.java.dev.jna:jna:5.19.1
+| +--- rs.wordpress.api:kotlin:1434-93b8ac510865b086e6b3588c3e816b08a65d020c
+| | +--- com.squareup.okhttp3:okhttp:5.4.0 (*)
+| | +--- com.squareup.okhttp3:okhttp-tls:5.4.0 (*)
+| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 -> 1.11.0 (*)
+| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
+| \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.21 (*)
-\--- rs.wordpress.api:android:1434-e7e17e7168b28116f3c7975ac8e20757259589cb (*)
+\--- rs.wordpress.api:android:1434-93b8ac510865b086e6b3588c3e816b08a65d020c (*) |
Base automatically changed from
fix/cmm-2152-newstats-visitors-dedicated-call
to
trunk
July 16, 2026 08:13
For Last6Months/Last12Months the chart derived its previous window by whole-month subtraction while the bottom row used an exact day-span mirror, so the header and the bottom row showed contradictory Views % change on the same card. Share the chart's config-based previous-window rule so both fetches compare against identical windows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dedicated bottom-stats call passed a mid-bucket startDate, so for month/year units the API truncated the first bucket's views (additive) while leaving visitors (unique) at the full-bucket value. The bottom row's Views therefore under-counted and disagreed with the chart header. Mirror the chart's fetch (unit + quantity + endDate, no startDate) so the API returns full buckets and the totals match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hourly window ended at the next day's 00:00, so the API returned 01:00→00:00 buckets — dropping today's 00:00 hour and under-counting the chart total versus the day-level bottom row. End the window at "<day> 23:00:00" instead so its 24 buckets cover 00:00–23:00 of the actual day (and yesterday for the previous window). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chart's stats/visits call already returns visitors/likes/comments/posts per bucket, so request those fields and reuse the same response to populate the bottom row instead of a separate dedicated call — halving the card's network calls (2 instead of 4) for every period except single-day ones. Single-day periods (Today, single-day Custom) render an hourly chart whose response only carries views, so they keep fetching the bottom row from a dedicated day-level call, matching what the web app does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… de-dup The Views card filled the bottom row from the chart response for every multi-day period. That over-counts unique Visitors for a Custom range beyond two years: the chart uses MONTH buckets, so a visitor active across several months is counted once per month, whereas the bottom row should de-duplicate per year. Views/Likes/Comments/Posts are additive and unaffected. Fill the bottom row from the chart only for the fixed periods (Last7Days/Last30Days/Last6Months/Last12Months), whose chart bucket already matches the unit the bottom row needs. Today and all Custom ranges now fetch the bottom row from the dedicated call, which coarsens to YEAR beyond two years so visitors are de-duplicated per year. This also drops the now-unreachable fixed-period branches from calculateBottomStatsRange (they no longer call it) and replaces isSingleDayPeriod with an exhaustive fillsBottomFromChart predicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… row For a Custom range over two years the chart used MONTH buckets while the bottom-row totals coarsen to YEAR, so the header's Views (chart) and the bottom row's Views (dedicated call) covered different, misaligned windows and could disagree on the same card. Pass allowYear = true for the custom chart window too, so the chart and the bottom row share one unit (YEAR): their Views totals — and their per-year visitor de-duplication — now always agree. Shorter ranges (day/month) are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes from review of this branch. unitAndQuantityFor derived its quantity from ChronoUnit.MONTHS/YEARS.between, which count whole units elapsed between the endpoints rather than the calendar buckets the window spans. The API returns `quantity` buckets ending at the bucket holding the end date, so whenever the end's day-of-month fell before the start's, the window's first bucket was silently dropped: Jan 15..Mar 5 elapses one whole month but spans three, so the chart and the bottom row both fetched Feb+Mar and lost January. Count spanned buckets instead. Both the chart and the bottom-row range share this function, so they kept agreeing with each other while under-counting; they now agree on the right total. fetchHourlyViews still applied the "endDate is exclusive for hourly queries" workaround that a9ac20a disproved, leaving the Today's Stats card with the same off-by-one hour the Views card just fixed: it asked for the next day at 00:00, dropping the day's own 00:00 bucket in favour of the following day's. Route it through formatApiEndDate so both callers model the API identically. Finally, fetchBottomStats's KDoc still described the routing as it stood at 80551d5, before 5bd3e0e sent every Custom range to the dedicated call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nges fillsBottomFromChart excluded every Custom range on the grounds that a range beyond two years needs yearly visitor de-duplication the chart's monthly buckets can't give. a4dc42f invalidated that: the custom chart window now coarsens to YEAR past two years, so for any multi-day Custom range the chart and the bottom row derive their unit, quantity and windows from the very same rules and issue byte-identical requests. Those periods were paying for two calls that duplicated the chart's own — the exact redundancy this branch set out to remove, on the one period that never got it. Route multi-day Custom through the chart response like the fixed periods, and keep only the single-day periods (Today, a Custom range whose start and end are the same day) on the dedicated call: their chart is hourly, and an hourly response populates `views` alone. This leaves calculateBottomStatsRange's coarser units unreachable from the card, since only single-day windows now reach it. The span-based rule stays anyway, so a caller that does need a standalone total for a longer window gets a correct one rather than a silently wrong bucket; the KDoc records that. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Custom range over two years renders every number on the Views card wrong -- chart, header total, % change and bottom row alike. For the range 2024-01-01..2026-06-29 the app already computed unit=year, quantity=3 and date=2026-06-29, identical to the web app, but omitted `start_date`: without it the API does not anchor its year buckets to the requested window. YEAR windows now send their own real start, matching web's unit=year&date=<end>&start_date=<start>&quantity=<n>. DAY and MONTH windows still send none. They are correct from unit + quantity + endDate, and a mid-bucket start_date there truncates the first bucket's views (additive) while leaving visitors (a per-bucket unique) at the full-bucket value. That asymmetry no longer splits the header from the bottom row -- both read one response now -- but it is still a reason not to send a start_date where nothing needs one. Sending start_date forces a second fix: a window's quantity and its start_date must describe the same window, and fetchStatsForPeriod sent the *current* window's quantity on the *previous* request. The previous window mirrors the current one's day span, which always resolves to the same unit but not always to the same bucket count -- 2023-12-31.. 2026-01-01 spans four years while its mirror spans three, so the API returned 2020-2023 and folded an extra year into the previous total. PeriodDateRange now carries a quantity per window, derived from that window's own span. Only Custom ranges coarsened to MONTH or YEAR change behaviour; every other period's two windows span equal buckets by construction, and tests pin start_date to null on the fixed periods so the YEAR rule cannot leak into paths that already work. Also drops the now-dead `allowYear` parameter and refreshes the KDocs the change falsified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Custom range over two years is charted in YEAR buckets, but its axis labels rendered as "Jan, Jan, Jan" and its legend as "Jan - Jun". The repository already picks StatsUnit.YEAR correctly and then dropped the value at buildPeriodStatsSuccess, so the ViewModel re-derived granularity from a day-count threshold that only distinguishes day from month: anything past 31 days got "MMM". The period string could not disambiguate either, since the API returns a full ISO date for DAY, MONTH and YEAR buckets alike. Carry the unit through PeriodStatsResult.Success so labels and the legend format from the granularity actually requested rather than guessing it a second time. This avoids duplicating the repository's two-year threshold into the ViewModel, which is how the two sides drifted apart to begin with. The period string still decides how to parse a bucket, as the API returns both "yyyy-MM" and "yyyy-MM-dd" shapes. DAY, MONTH and HOUR output is unchanged; only YEAR differs. Adds the first coverage for chart labels, including locale-independent locks that the year pattern cannot leak into the other units. Drill-down still makes the same day-count guess, so tapping a year bar opens that January. Left as a follow-up: it changes an interaction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a8de96 to
d600564
Compare
Reuse ShimmerBox for the loading placeholders, dedup the previous-window mirror math, collapse the copy-paste bottom-stats aggregate mapping, and drop the redundant type-check in onChartTypeChanged. Convert formatDataPointLabel to an expression body to stay under the ReturnCount limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #23101 +/- ##
==========================================
+ Coverage 37.63% 37.67% +0.04%
==========================================
Files 2341 2341
Lines 127275 127315 +40
Branches 17648 17659 +11
==========================================
+ Hits 47895 47962 +67
+ Misses 75461 75432 -29
- Partials 3919 3921 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Description
Polishes the new-stats Views card, fixing a series of period-specific accuracy bugs where the header total, the header % change, the chart, and the bottom-row totals could disagree with each other (and with the web app), and then cutting the card's network calls in half.
What's fixed / changed
1. The bottom row is filled from the chart's own call (fewer network calls)
The chart's
stats/visitscall already returns visitors/likes/comments/posts per bucket. For every period whose chart is not hourly — the fixed periods and any multi-day Custom range — the card now requests those fields and reuses that same response to populate the bottom row instead of issuing a separate dedicated call: 2 network calls instead of 4.This also makes the header and the bottom row agree by construction: they are now literally the same response, so no pair of windows or units can drift apart.
Single-day periods (Today, single-day Custom) render an hourly chart whose response only carries
views, so they alone keep fetching the bottom row from a dedicated day-level call (matching the web app) — 4 calls.2. The window's first bucket is no longer dropped
unitAndQuantityForderived its quantity fromChronoUnit.MONTHS/YEARS.between, which count the whole units elapsed between the endpoints rather than the calendar buckets the window spans. Since the API returnsquantitybuckets ending at the bucket holding the end date, the window's first bucket was silently dropped whenever the end's day-of-month fell before the start's:Jan 15..Mar 5elapses one whole month but spans three, so the card fetched Feb+Mar and lost January entirely — from the chart, the header total and the bottom row alike. Roughly half of all custom ranges longer than 31 days were affected. It now counts spanned buckets.3. Correct hourly window for single-day periods
The Today chart requested the hourly window ending at the next day's
00:00, so the API returned01:00→00:00buckets — dropping today's00:00hour and under-counting the chart total vs the day-level bottom row. The window now ends at"<day> 23:00:00", covering the calendar day's00:00–23:00(and yesterday for the previous window), matching the web app.The same off-by-one hour lived in
fetchHourlyViews, which still applied an "endDate is exclusive for hourly queries" workaround; it now shares the oneformatApiEndDatehelper, so the Today's Stats card gets the same fix and the file no longer holds two contradictory models of the endpoint.4. No first-bucket truncation in the dedicated bottom call
The dedicated bottom-stats call passed a mid-bucket
startDate, so for month/year units the API truncated the first bucket's views (additive) while leaving visitors (per-bucket unique) at the full value. The call now mirrors the chart (unit + quantity + endDate, withstart_dateonly where a YEAR window needs it — see #6).5. The Custom chart coarsens to YEAR beyond two years
For a Custom range over two years the chart used MONTH buckets while the bottom-row totals coarsen to YEAR, so the two covered different, misaligned windows. The custom chart window now shares the bottom row's exact rule, so a long Custom range keeps one unit across the whole card — and, since #1 has it fill the row from that same response, its visitor uniques are de-duplicated per year rather than counted once per month.
6. Long Custom ranges send
start_date, and each window carries its own quantityA Custom range over two years rendered every number on the card wrong — chart, header total, % change and bottom row alike. The app already computed
unit=year,quantity=3anddate=<end>identically to the web app but omittedstart_date; without it the API doesn't anchor its year buckets to the requested window, so it returned the wrong buckets. YEAR windows now send their own real start, matching web'sunit=year&date=<end>&start_date=<start>&quantity=<n>. DAY and MONTH windows still send none (they're correct from unit + quantity + endDate, and a mid-bucketstart_datethere is the truncation described in #4).Sending
start_dateforced a second fix: a window'squantityand itsstart_datemust describe the same window, but the previous request reused the current window's quantity. The previous window mirrors the current one's day span, which always resolves to the same unit but not always to the same bucket count —Dec 31 2023..Jan 1 2026spans four years while its mirror spans three, so the API folded an extra year into the previous total and skewed the % change.PeriodDateRangenow carries a quantity per window, each derived from its own span.7. YEAR chart buckets are labeled with the year, not the month
A Custom range charted in YEAR buckets rendered its axis labels as "Jan, Jan, Jan" and its legend as "Jan - Jun", because the ViewModel re-derived granularity from a day-count that only distinguishes day from month. The requested unit is now carried through
PeriodStatsResult.Success, so labels and the legend format from the granularity actually requested rather than guessing it a second time. DAY/MONTH/HOUR output is unchanged.Known follow-up: drill-down still guesses granularity from a day-count, so tapping a year bar opens that January. Deferred here because it changes an interaction.
Cleanup
Reuse
ShimmerBoxfor the loading placeholders, dedup the previous-window mirror math, collapse the copy-paste bottom-stats aggregate mapping, and satisfy detekt.All of the above are covered by unit tests in
StatsRepositoryTestandViewsStatsViewModelTest.Note on the commit history
The branch is best read as its end state rather than commit-by-commit: a few commits supersede earlier ones. In particular, the Custom range's bottom-row routing was first moved to the dedicated call and then returned to the chart response once the custom chart window and the bottom row were made to share one unit, quantity and window; and an earlier "align previous windows" step is moot now that the fixed periods make no dedicated bottom call at all.
Testing instructions