Skip to content

Premium Analytics: support view_stats-level dashboard access - #50889

Merged
chihsuan merged 10 commits into
trunkfrom
wooa7s-1779-view-stats-access
Jul 31, 2026
Merged

chihsuan merged 10 commits into
trunkfrom
wooa7s-1779-view-stats-access

Conversation

@chihsuan

@chihsuan chihsuan commented Jul 29, 2026 •

Copy link
Copy Markdown
Member

Fixes WOOA7S-1779

Proposed changes

Premium Analytics replaces the Stats UI, but its menu and supporting routes were limited to administrators. That prevented users granted access through Allow Jetpack Stats to be viewed by from opening the replacement dashboard.

This PR aligns dashboard and store-report access with the existing Jetpack Stats and WooCommerce permissions:

Surface Required capability
Analytics menu, dashboard, and support routes manage_options or view_stats
Store reports and CSV exports manage_options or view_woocommerce_reports
Store section and store, orders, coupons, bookings, and visitors widgets Store-report access above; otherwise hidden
WordAds data Unchanged (manage_options or activate_wordads)

A dedicated jetpack_view_analytics meta capability represents the dashboard rule because WordPress menu registration accepts one capability and view_stats is not registered in every supported environment.

This also brings store reports in line with WooCommerce Analytics (WOOA7S-551).

Follow-ups and known gaps:

  • A shop manager still needs Stats access to open Premium Analytics. Broader shop-manager access is tracked in WOOA7S-1823.
  • WordAds widgets are still offered to readers who only have view_stats, whose data the proxy then denies. Tracked in WOOA7S-1727.
  • ?page=jetpack-premium-analytics entry point not gated WOOA7S-1821
  • During initial sync, a user with only view_stats may reach the syncing screen while its status endpoint remains administrator-only.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

Use a site with Premium Analytics available and WooCommerce active, and create two test users: one Editor and one Shop manager.

Stats access lives in Jetpack → Settings → Traffic → Jetpack Stats (expand the card). It renders two role checkbox groups — Count logged in page views from and Allow Jetpack Stats to be viewed by. Only the second one grants view_stats.

  1. Grant the Editor role Stats access, log in as the editor, and confirm:
    • The Analytics menu appears and wp-admin/admin.php?page=jetpack-premium-analytics-wp-admin loads.
    • The section tabs are Traffic, Insights, and Subscribers, with no Store tab.
    • Customize → Add widget offers no store or visitors widgets — searching for "Store performance", "Sales by coupon", "Visitors by location", or "Sessions by device" returns nothing.
  2. Remove the Editor role's Stats access and confirm the menu disappears and the dashboard URL returns 403.
  3. Log in as the Shop manager before granting Stats access and confirm the menu is absent and the dashboard URL returns 403 — store-report access on its own does not open the dashboard.
  4. Grant the Shop manager Stats access, log in again, and confirm the Store tab appears and its widgets render data rather than an error state.
  5. Log in as an administrator and confirm nothing regressed: all four tabs, the store and visitors widgets in Customize, and Store → Orders over time → Download CSV, which should POST to /jetpack-premium-analytics/v1/reports/csv-export and produce a file.

Optional, as the Stats-enabled editor: /wpcom/v2/dashboards/jetpack-premium-analytics_dashboard/sections and /wpcom/v2/widget-modules return 200, the latter with the store-category widgets filtered out, while /jetpack-premium-analytics/v1/proxy/v2/analytics/... returns 403.

@github-actions

github-actions Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the wooa7s-1779-view-stats-access branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack wooa7s-1779-view-stats-access
bin/jetpack-downloader test jetpack-mu-wpcom-plugin wooa7s-1779-view-stats-access

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@jp-launch-control

jp-launch-control Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 6 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/premium-analytics/src/widget-availability.php 48/56 (85.71%) 2.79% 1 ❤️‍🩹
projects/packages/premium-analytics/src/class-analytics.php 75/108 (69.44%) 0.29% 0 💚
projects/packages/premium-analytics/src/class-dashboard-support-routes.php 8/11 (72.73%) 2.73% 0 💚
projects/packages/premium-analytics/src/dashboard-sections.php 102/108 (94.44%) 0.05% 0 💚
projects/packages/premium-analytics/src/widget-modules.php 27/50 (54.00%) 0.15% -1 💚

1 file is newly checked for coverage.

File Coverage
projects/packages/premium-analytics/src/class-capabilities.php 9/9 (100.00%) 💚

Full summary · PHP report · JS report

@github-actions github-actions Bot added the Docs label Jul 29, 2026
@chihsuan
chihsuan marked this pull request as ready for review July 29, 2026 07:56
@chihsuan
chihsuan requested a review from a team July 29, 2026 07:56
@chihsuan
chihsuan requested review from a team as code owners July 29, 2026 07:56
@chihsuan chihsuan added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Jul 29, 2026
kangzj
kangzj previously approved these changes Jul 31, 2026

@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.

Nice, thorough capability layer — well-tested (393 passing PHP tests, dedicated Capabilities_Test.php pinning the store-report helper to the proxy's own check). One low-severity inline gap worth a look, but not a blocker.

'permission_callback' => static function () {
return current_user_can( 'manage_options' );
},
'permission_callback' => array( Capabilities::class, 'current_user_can_view_analytics' ),

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.

This route's permission check was widened from manage_options to Capabilities::current_user_can_view_analytics() (manage_options OR view_stats), but the handler it guards (get_dashboard_default_layout_for()) doesn't re-check the store-report capability when the requested dashboard resolves to woocommerce/store.

A view_stats-only reader — exactly the population this PR admits to the dashboard but explicitly not to store data (see test_omits_woocommerce_dashboard_section_from_a_view_stats_reader) — can call GET /wpcom/v2/dashboards/traffic/default-layout?name=woocommerce/store. WP's get_param() resolves query-string values ahead of the URL route capture (the same shadowing risk Api_Proxy_Controller::validate_data_endpoint()'s ?endpoint= comment already defends against), so $request['name'] resolves to 'woocommerce/store' regardless of the URL path, and the Store tab's default widget layout (jpa/store-performance, jpa/orders-over-time, etc.) gets seeded with no current_user_can_view_store_reports() check.

Impact is limited — only default widget-type identifiers leak (strings already public in the plugin source/JS bundles), not real store data, since the proxy call for actual data stays correctly gated on view_woocommerce_reports. But it's inconsistent with the stated design goal in current_user_can_view_store_reports()'s own docblock ("surfaces backed by the prefix are hidden from readers who fail it"), and no test currently covers this route with a $name-shadowed request.

Suggest gating on Capabilities::current_user_can_view_store_reports() when the resolved section is woocommerce/store, mirroring what is_woocommerce_dashboard_section_available_to_current_user() and the /sections/{section}/default-layout route already do via Dashboard_Section::is_available().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 66833cd

*
* @since $$next-version$$
*/
class Capabilities {

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.

Love it how we are centralizing the capabilities check 👍

@kangzj

kangzj commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Forms E2E test is failing but it doesn't seem related:

Error: Unable to download artifact(s): Artifact not found for name: jetpack-core.tar.xz

@chihsuan
chihsuan merged commit 47edb88 into trunk Jul 31, 2026
80 checks passed
@chihsuan
chihsuan deleted the wooa7s-1779-view-stats-access branch July 31, 2026 07:09
@github-actions github-actions Bot added [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. and removed [Status] Needs Review This PR is ready for review. labels Jul 31, 2026
louwie17 added a commit that referenced this pull request Aug 4, 2026
The Store section is now gated on the current user's capabilities (#50889), so
the availability filter alone no longer surfaces it in the built-in sections
test.

Drop the removed `containerElement` prop from `DateFiltersPanel`, and take
trunk's `getSectionSubtitle` for the header subtitle instead of the year-only
label this branch added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2
louwie17 added a commit that referenced this pull request Aug 5, 2026
The Store section is now gated on the current user's capabilities (#50889), so
the availability filter alone no longer surfaces it in the built-in sections
test.

Drop the removed `containerElement` prop from `DateFiltersPanel`, and take
trunk's `getSectionSubtitle` for the header subtitle instead of the year-only
label this branch added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2
louwie17 added a commit that referenced this pull request Aug 5, 2026
…on (#50930)

* Premium Analytics: add a per-section all-time and yearly date selection

Make the date-filter surface a per-section configuration and turn it on for
Insights, which reports on whole history rather than a rolling window.

Sections gain a `date_filter` arg (`range`, the default, or `year`), exposed
in the sections REST response and its new schema. The dashboard renders
`DateYearFilter` for `year` sections and keeps `DateFiltersPanel` everywhere
else, and reconciles the shared `?preset=` param with whichever surface the
active section shows so a section switch never leaves the control unable to
represent the selection.

Builds on #50754, which shipped the year presets and the filter component
without mounting them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

* Premium Analytics: make the frontend section date_filter optional

WPCOM's public-api registers the sections route from its own checkout, so a
Simple site can be served a payload built before the field existed. A required
field modelled that as impossible. `resolveDateFilterSurface` already treats a
missing value as the date-range surface, so nothing changes at runtime.

Drops the fixture lines the required field had forced into two files that have
nothing to do with date filters, and covers the now-reachable case: a section
served without the field resolves to the range surface.

The PHP side is unchanged — `Dashboard_Section::$date_filter` already defaults
to `range` and the schema already declares that default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

* Premium Analytics: keep the section date filter working on current trunk

The Store section is now gated on the current user's capabilities (#50889), so
the availability filter alone no longer surfaces it in the built-in sections
test.

Drop the removed `containerElement` prop from `DateFiltersPanel`, and take
trunk's `getSectionSubtitle` for the header subtitle instead of the year-only
label this branch added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

* Premium Analytics: apply the date selection review feedback

Drop `resolveDateFilterSurface` and compare `date_filter` with the year
constant directly: `year` is the only opt-in surface, so a missing or unknown
value already falls through to the range UI, and the helper's `unknown`
parameter fought the closed union. Its coverage moves to the hook, where the
fallback is a behaviour rather than a fixture shape.

Scope the Phan suppression to the array entry it is reported on instead of the
whole function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

* Premium Analytics: explain both Store section gates in the test

The filter stands in for WooCommerce being active and the admin user satisfies
the capability check; the section needs both, so neither line is redundant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

* Premium Analytics: drop the comparison from the year surface subtitle

The year surface has no comparison control, so the header should not announce
"vs. Previous period" for a comparison the reader cannot switch off from there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAX3YkQxDLLXhKXjft6qU2

---------

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

Labels

Docs [Package] Premium Analytics [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants