Skip to content

Odyssey Stats: Hide Jetpack upsells for products the site already owns - #112461

Merged
kangzj merged 1 commit into
trunkfrom
fix/stats-upsell-entitlement-check
Jul 10, 2026
Merged

Odyssey Stats: Hide Jetpack upsells for products the site already owns#112461
kangzj merged 1 commit into
trunkfrom
fix/stats-upsell-entitlement-check

Conversation

@dognose24

@dognose24 dognose24 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Fixes STATS-286

Proposed Changes

  • Invert the visibility check for the Stats page Jetpack upsell section: each upsell now declares the ownedFeatures entitlement slugs that mean the product is already owned (directly or via a bundling plan), and the card is hidden when the site has all of them.
  • Previously an upsell was shown when the site was missing any one of the product's feature slugs, so a site on Jetpack Security (which bundles VaultPress Backup) was still prompted to upgrade to Backup — only Jetpack Complete owned every slug and escaped the prompts. The redundancy was only surfaced at checkout ("The product you are about to purchase, VaultPress Backup, is already included in this plan.").
  • Extract the filter into a pure filterUpsellsBySiteFeatures() helper and add unit tests covering the Security-plan scenario from the issue.

Before / after, per card

The old features arrays were product selling-point dumps used as ownership checks — "show the card if the site lacks any slug in the list". The new ownedFeatures are the minimal entitlement set meaning "the product's substance is already owned" — "hide the card if the site has all of them".

Card Old check (show if missing any) New check (hide if has all) On a Jetpack Security Daily site
Backup backups, full-activity-log, real-time-backups, priority_support backups Old: missing real-time-backups (Daily = daily backups) → wrongly shown. New: has backups → hidden.
Security 18 slugs incl. unrelated ones (wordads, google-analytics, core/audio, simple-payments, …) plus both backups-daily and real-time-backups backups, scan, antispam Old: missing real-time-backups, google-my-businessSecurity owners were upsold Security. New: has all three → hidden.
Search search, instant-search search unchanged behavior
VideoPress videopress, videopress-1tb-storage videopress unchanged behavior
Boost 6 paid-Boost slugs cloud-critical-css unchanged behavior
Social 3 slugs social-enhanced-publishing unchanged behavior

Because the old Security list contained both mutually-exclusive backup granularities (backups-daily and real-time-backups), it was unsatisfiable for any plan except Complete — matching the report that the card showed "for any site that isn't on the Jetpack Complete plan".

Why are these changes being made?

  • On a site that already owns Jetpack Security, the Stats page upsell card ("Enhance … with Jetpack Security, Performance, and Growth tools") prompted a Backup upgrade even though the plan already includes VaultPress Backup. The card should key off actual entitlements rather than "not on Complete". See STATS-286 for the full report.

Testing Instructions

  • yarn test-client client/my-sites/stats/jetpack-upsell-section — 4/4 tests pass.
  • On a Jetpack site without any paid upgrades, open Odyssey Stats (wp-admin → Jetpack → Stats, Traffic tab) and scroll to the upsell section: all six cards (Security, Backup, Search, VideoPress, Boost, Social) should still appear.
  • On a Jetpack site with Jetpack Security (or another plan bundling Backup/Scan/Anti-spam): the Security and Backup cards should no longer appear, while unowned products (e.g. Search, VideoPress) still show.
    • To reproduce the exact STATS-286 scenario you need the legacy Jetpack Security Daily plan, which is no longer purchasable — attach it to a test site with internal tooling, or simulate any plan by filtering the feature source: add_filter( 'option_jetpack_active_plan', fn( $plan ) => array_merge_recursive( $plan, [ 'features' => [ 'active' => [ 'backups', 'scan', 'antispam' ] ] ] ) ); in an mu-plugin. The section reads entitlements from these slugs, so the filter exercises the same code path as a real purchase.
  • On a site with Jetpack Complete: no upsell cards should appear.

Test results (verified on a self-hosted Jetpack site, local Docker)

Verified against real plan data (the section reads entitlements from the jetpack_active_plan feature slugs that stats-admin inlines into the page):

  • ✅ With VaultPress Backup (10GB) + Jetpack Search Free: the Backup and Search cards are hidden (backups / search are in the site's active features); Security, VideoPress, Boost and Social still show.
  • ✅ With Jetpack Security Daily attached (the exact plan from STATS-286 — its active features include backups, backups-daily, scan, antispam but not real-time-backups): only VideoPress, Boost and Social show. Under the previous logic this site was shown both the Backup and Security cards, because the missing real-time-backups slug satisfied the "any feature missing" check.
  • ✅ Confirmed the feature slug names used in ownedFeatures (backups, scan, antispam, search) match what the plan actually reports.
  • Note: Jetpack Search Free reports both search and instant-search, so the Search card is also hidden for free-tier Search sites — there is no feature slug distinguishing free from paid Search; keying off entitlement is the intended behavior here.

My purchases

截圖 2026-07-09 晚上10 37 07

Upsell cards

Before After
截圖 2026-07-09 晚上10 30 14 截圖 2026-07-09 晚上10 29 32

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)? (Self-hosted Jetpack — the only environment where this Odyssey-gated section renders.)
  • Have you checked for TypeScript, React or other console errors?
  • For UI changes, have you tested the affected components in dark mode?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

🤖 Generated with Claude Code

The Odyssey Stats upsell section showed a product whenever the site was
missing any one of the product's feature slugs, so a site on Jetpack
Security (which bundles VaultPress Backup) was still prompted to buy
Backup, and only checkout revealed the product was already included.
Invert the check: each upsell now lists the entitlement features that
mean the product is owned, and the card is hidden when the site has all
of them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@matticbot

Copy link
Copy Markdown
Contributor

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

Async-loaded Components (~179 bytes removed 📉 [gzipped])

Details
name                                                      parsed_size           gzip_size
async-load-calypso-my-sites-stats-jetpack-upsell-section       -443 B  (-3.5%)     -179 B  (-3.9%)

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@dognose24
dognose24 marked this pull request as ready for review July 9, 2026 14:30
@dognose24
dognose24 requested review from a team and kangzj July 9, 2026 15:03
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jul 9, 2026
@dognose24 dognose24 self-assigned this Jul 9, 2026
@dognose24 dognose24 changed the title Stats: hide Jetpack upsells for products the site already owns Odyssey Stats: Hide Jetpack upsells for products the site already owns Jul 9, 2026
@dognose24
dognose24 requested a review from Copilot July 10, 2026 00:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the Odyssey Stats Jetpack upsell section to hide upsell cards when the site already owns the relevant product entitlements (including via bundled plans), addressing redundant upsells like prompting Backup on Jetpack Security sites.

Changes:

  • Replaces the old “missing any feature → show upsell” logic with ownedFeatures and a “has all owned features → hide upsell” check.
  • Extracts the visibility logic into a pure filterUpsellsBySiteFeatures() helper and wires it into the section.
  • Adds unit tests covering the Jetpack Security (Daily) scenario and other key ownership combinations.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
client/my-sites/stats/jetpack-upsell-section/upsell-card/available-upsells.tsx Switches upsell ownership modeling to ownedFeatures and introduces the reusable filtering helper.
client/my-sites/stats/jetpack-upsell-section/index.tsx Uses the new helper to compute visible upsells based on active site entitlements.
client/my-sites/stats/jetpack-upsell-section/test/available-upsells.tsx Adds unit tests to ensure bundled-plan ownership hides the correct upsell cards.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// owned. The upsell is hidden when the site has all of them.
ownedFeatures: string[];
checkoutSlug: string;
checkoutUrl: string | null;
Comment on lines 110 to +113
export function getUpsellFeatureSlugs(): string[] {
const upsells = getAvailableUpsells();
return upsells.flatMap( ( upsell ) => upsell.features );
return upsells.flatMap( ( upsell ) => upsell.ownedFeatures );
}

@kangzj kangzj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tests well and looks good.

I think we should probably come back later and see if there are existing feature sets to be used rather than creating our own, which would be hard to keep in sync.

Thanks for the fast fix @dognose24. Appreciate it!

@kangzj
kangzj added this pull request to the merge queue Jul 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 10, 2026
@kangzj
kangzj added this pull request to the merge queue Jul 10, 2026
@dognose24

Copy link
Copy Markdown
Contributor Author

@kangzj Thanks for the review! I dug into your "existing feature sets" question — findings:

Where the old lists came from. The feature lists were introduced with the section in #99048 (Feb 2025) and the Security list was expanded to 18 slugs three days later in #99471. The shape of that list (including unrelated slugs like wordads, google-analytics, core/audio) suggests it was a dump of a legacy plan's features.active at the time, and it was never updated since — e.g. it still contains google-my-business, which plans no longer grant. So the "hard to keep in sync" concern had already materialized.

Existing feature sets. Every entitlement slug this PR uses already has a constant in @automattic/calypso-products: WPCOM_FEATURES_BACKUPS, WPCOM_FEATURES_SCAN, WPCOM_FEATURES_ANTISPAM, WPCOM_FEATURES_CLASSIC_SEARCH, WPCOM_FEATURES_VIDEOPRESS, FEATURE_CLOUD_CRITICAL_CSS, FEATURE_SOCIAL_ENHANCED_PUBLISHING. And siteHasFeature() + these constants is the established ownership-check pattern elsewhere (e.g. client/my-sites/scan/wpcom-scan-upsell.tsx, client/jetpack-cloud/sections/landing/selectors.ts). What doesn't exist anywhere on the client is a maintained product → entitlement grouping — the bundling relationships live server-side in WPCOM_Features — so the per-card ownedFeatures grouping stays, but the slug spelling can come from the shared constants.

Follow-up. This PR is already in the merge queue (its branch is locked), so I'll land the constant swap — together with Copilot's two nits (optional checkoutUrl instead of the as Product[] assertion, and deduping getUpsellFeatureSlugs()) — as an immediate follow-up PR right after this merges.

Merged via the queue into trunk with commit 05484ac Jul 10, 2026
25 checks passed
@kangzj
kangzj deleted the fix/stats-upsell-entitlement-check branch July 10, 2026 01:50
@github-actions github-actions Bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jul 10, 2026
@dognose24

Copy link
Copy Markdown
Contributor Author

Follow-up with the constant swap + Copilot nits is up: #112497

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants