Skip to content

CMM-2341: Show a plan upsell for gated stats instead of a permission error - #23389

Open
adalpari wants to merge 3 commits into
trunkfrom
cmm-2341-android-missing-upsell-in-stats
Open

adalpari wants to merge 3 commits into
trunkfrom
cmm-2341-android-missing-upsell-in-stats

Conversation

@adalpari

@adalpari adalpari commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

This Pr is showing a plan upsell for stats that are not available to the user. Previously it was a plan message and a wp-admin CTA.

Screenshot_20261002-113759

Description

Fixes CMM-2341.

Regions, Cities, Devices and UTM are gated behind a paid plan. WordPress.com
answers those requests on an unsupported plan with 403 and a body of
{"error":"unauthorized","message":"The plan for this site does not allow fetching Device stats"}. StatsDataSourceImpl.classifyError only looked at the
HTTP status, so the gate collapsed into AUTH_ERROR and the card told the user
"You don't have permission to view these stats" and offered a WP Admin button
— neither of which is actionable. iOS already handles this
(Modules/Sources/JetpackStats/Services/StatsFeatureGateError.swift +
FeatureGateBannerView); this is a port of that behaviour.

Detection. StatsApiError.kt gains isStatsGatedByPlan() next to the
existing isStatsUnavailableForSite(), reusing the same regex parse of the
WordPress.com error field. A new StatsErrorType.PLAN_GATED is checked in
classifyError ahead of the status-code fallback, in both the WpError and
UnknownError branches.

Plumbing. isPlanGated travels alongside the existing isAuthError through
the Country/Region/City, Devices and UTM repository results into the Locations,
Devices and UTM card UI states.

UI. StatsCardErrorContent takes an optional onExplorePlans; when it is
set, a lock, the upgrade message and an "Explore Plans" button replace the red
error text and Retry button — there is nothing to retry until the plan changes.
The card's type selector stays visible, so you can switch back to an ungated
breakdown.

Upgrade URL. New SiteModel.statsUpgradeUrl() returns
wordpress.com/pricing/ for WordPress.com and Atomic sites and
cloud.jetpack.com/pricing for Jetpack sites, matching iOS's makeUpgradeURL.
It opens in a Custom Tab and fires a new
stats_feature_gate_explore_plans_tapped event with a feature property.

Notes for the reviewer

  • ⚠️ WordPress.com reuses unauthorized for a user who genuinely cannot view a
    site's stats, so that case now reads "upgrade your plan" too. iOS makes the
    same trade-off: the plan gate is by far the common cause, and the permission
    message this replaces was no more accurate for gated sites. Documented in a
    comment on isStatsGatedByPlan.
  • UTM is included even though the issue only names Regions/Cities and Devices —
    it is the same endpoint gate, and iOS's StatsFeatureGateError.featureName
    enumerates it alongside devices and locations.
  • The Locations and UTM detail screens are deliberately untouched: both are only
    reachable through "Show All", which a gated card never renders.

Testing instructions

Because the gate depends on the site's plan, the quickest way to exercise it is
to force the gated result. In StatsRepository, make fetchDevicesData and
fetchRegionViews return their Error(..., isPlanGated = true) early, then:

  1. Open Stats on any non-premium site and go to the Traffic tab.
  2. Scroll to the Locations card and tap Regions.
  • Verify a lock, "Upgrade your plan to get access to advanced analytics" and
    an Explore Plans button appear, and that the
    Countries/Regions/Cities selector is still usable.
  1. Open Stats on a premium site and go to the Traffic tab.
  2. Scroll to the Locations card and tap Regions.
  • Verify you can see the stats

WordPress.com answers region, city, device and UTM stats requests on
unsupported plans with an "unauthorized" error, which the card treated as a
generic permission problem - telling the user they lack permission and offering
WP Admin, neither of which is actionable. Detect the plan gate and show the same
upgrade prompt iOS does instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dangermattic

dangermattic commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator
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

wpmobilebot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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
Versionpr23389-3e173ae
Build Number1498
Application IDorg.wordpress.android.prealpha
Commit3e173ae
Installation URL65hiohi7am650
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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
Versionpr23389-3e173ae
Build Number1498
Application IDcom.jetpack.android.prealpha
Commit3e173ae
Installation URL13nam557p2l7o
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.

The gate was detected from the WordPress.com `unauthorized` error code alone,
but that code is also what every stats endpoint returns from its permission
check, which runs before the plan check. A user whose role doesn't allow
viewing stats - the Jetpack default is administrators only - was told to
upgrade, which no purchase would have fixed. Only the plan check names the
plan, so match the message too and let everything else fall back to the
regular permission error.

PLAN_GATED also carried the upsell copy as its message, and only the Locations,
Devices and UTM cards show the upsell UI. Any other card rendered "Upgrade your
plan..." as red error text with a Retry button that could never succeed. Give
the type a message that stands on its own as an error; the cards that show the
upsell supply its copy themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adalpari
adalpari marked this pull request as ready for review October 2, 2026 13:13
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 24.30556% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.42%. Comparing base (011d448) to head (3e173ae).

Files with missing lines Patch % Lines
.../android/ui/newstats/components/StatsCardCommon.kt 0.00% 46 Missing ⚠️
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 32 Missing ⚠️
...roid/ui/newstats/datasource/StatsDataSourceImpl.kt 0.00% 8 Missing ⚠️
.../android/ui/newstats/repository/StatsRepository.kt 66.66% 7 Missing ⚠️
.../wordpress/android/ui/newstats/utm/UtmViewModel.kt 0.00% 4 Missing ⚠️
...rdpress/android/ui/newstats/devices/DevicesCard.kt 0.00% 3 Missing ⚠️
...ess/android/ui/newstats/locations/LocationsCard.kt 0.00% 3 Missing ⚠️
...a/org/wordpress/android/ui/newstats/utm/UtmCard.kt 0.00% 3 Missing ⚠️
...ss/android/ui/newstats/datasource/StatsApiError.kt 83.33% 0 Missing and 1 partial ⚠️
...ss/android/ui/newstats/devices/DevicesViewModel.kt 66.66% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #23389      +/-   ##
==========================================
- Coverage   38.44%   38.42%   -0.02%     
==========================================
  Files        2298     2299       +1     
  Lines      127260   127362     +102     
  Branches    17990    18016      +26     
==========================================
+ Hits        48920    48941      +21     
- Misses      74285    74365      +80     
- Partials     4055     4056       +1     

☔ View full report in Codecov by Harness.
📢 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 requested a review from nbradbury October 2, 2026 13:29
The gated stats endpoints name the site by its numeric blog id, not by a
phrase: WordPress.com builds the body with
`sprintf( 'The plan for %d does not allow fetching Device stats', $blog_id )`.
The KDoc and every positive test fixture used "The plan for this site ..."
instead, a body the API never sends.

Detection was unaffected, because PLAN_GATE_MESSAGE_FRAGMENT starts after
the blog id. The risk was the next edit: the fragment reads as narrower
than the sentence the doc quotes, so tightening it to match that sentence
is a natural change, and it would have silently dropped the upsell on
every gated card while the fixtures kept passing.

Correct both, and say why the fragment has to stay short.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants