Skip to content

Premium Analytics: add Top posts & pages stats widget - #49568

Merged
louwie17 merged 8 commits into
trunkfrom
wooa7s-1489-stats-top-posts
Jun 26, 2026
Merged

Premium Analytics: add Top posts & pages stats widget#49568
louwie17 merged 8 commits into
trunkfrom
wooa7s-1489-stats-top-posts

Conversation

@louwie17

@louwie17 louwie17 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Fixes WOOA7S-1489

Proposed changes

Adds the jpa/stats-top-posts dashboard widget to Premium Analytics, the kickoff Stats card port. Rebased onto trunk: the presentational TopPostsWidget, the stats proxy data layer, and the designated useStatsTopPosts traffic hook/query have all since landed in trunk, so this PR is now just the widget glue that connects them.

  • widgets/top-posts/ — registers the jpa/stats-top-posts widget (framed presentation) with range/num attributes:
    • widget.json / widget.ts — widget metadata + type definition (date-range preset + result-count attributes; defaults to the trailing 7 days, ten results).
    • render.tsx — fetches through the designated useStatsTopPosts Stats traffic hook, resolves the range preset to an absolute window via computeDateRangeFromPreset, adapts the normalized report items ({ label, views, link, type }) to the TopPostRow[] the presentational TopPostsWidget expects, and filters by post type client-side. Runs inside WidgetRoot for the analytics query client.
    • package.json — widget package wiring (@jetpack-premium-analytics/data + widgets-toolkit).
    • __tests__/top-posts.test.tsx — render / post-type-filter / empty-state tests, driven through the stats proxy with @wordpress/api-fetch mocked.
  • packages/data — re-exports computeDateRangeFromPreset from the package root so the widget can resolve range presets.
  • tests/ — adds a style-stub.cjs + moduleNameMapper entry so widget render tests can pull in widgets-toolkit (whose transitive @automattic/ui / @automattic/charts CSS imports are otherwise skipped by jest's transformIgnorePatterns and parsed as JS).

The widget's num (result count) maps to the WPCOM stats API's max; multi-day ranges are summarized into a single bucket by the stats query layer.

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

No. It reads existing Jetpack Stats data through the existing jetpack-premium-analytics/v1 stats proxy via the designated useStatsTopPosts hook.

Testing instructions

Automated:

  • cd projects/packages/premium-analytics && pnpm test — full suite passes, including the three top-posts widget tests (render, post-type filtering, empty state).
  • pnpm run typecheck — clean.

Manual:

  • On a site with both Jetpack (connected, with stats) and Premium Analytics active, open the Premium Analytics admin page.
  • Add the "Top pages by views" widget to the dashboard and confirm it lists the top-viewed posts/pages with view counts, links open the posts in a new tab, and a site with no views shows "No views in this period."

@louwie17 louwie17 added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] Needs Review This PR is ready for review. labels Jun 12, 2026
@louwie17 louwie17 self-assigned this Jun 12, 2026
@github-actions

github-actions Bot commented Jun 12, 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-1489-stats-top-posts branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack wooa7s-1489-stats-top-posts
bin/jetpack-downloader test jetpack-mu-wpcom-plugin wooa7s-1489-stats-top-posts

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

github-actions Bot commented Jun 12, 2026

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!


Premium Analytics plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@louwie17
louwie17 marked this pull request as draft June 12, 2026 01:25
@jp-launch-control

jp-launch-control Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

@louwie17
louwie17 force-pushed the update/pa-introduce-customize-dashboard branch from 909996e to d060b2d Compare June 12, 2026 02:39
@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch from f1a7fee to 2899350 Compare June 12, 2026 03:22
@nerrad
nerrad force-pushed the update/pa-introduce-customize-dashboard branch from d060b2d to 5bb6dd9 Compare June 12, 2026 08:16
@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch from 13834c1 to df71ef4 Compare June 15, 2026 13:25
@nerrad
nerrad force-pushed the update/pa-introduce-customize-dashboard branch from 5bb6dd9 to 7ff7f71 Compare June 17, 2026 11:19
@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch from df71ef4 to a8ec740 Compare June 17, 2026 12:33
louwie17 added a commit that referenced this pull request Jun 18, 2026
Add a prop-driven TopPostsWidget and Storybook story under
Packages/Premium Analytics/Widgets/TopPosts, mimicking the
"Top posts & pages" widget from #49568 as a self-contained,
mock-data-backed component.

The widget renders the most-viewed posts/pages as an overlay
leaderboard (each row links to the published content) and supports
previous-period comparison with colored deltas, mirroring the overlay
comparison mode of the toolkit's LeaderboardChart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpN9Z3MTaLb4GxTNsGFaEQ
louwie17 added a commit that referenced this pull request Jun 19, 2026
* Premium Analytics: add Top posts & pages widget to Storybook

Add a prop-driven TopPostsWidget and Storybook story under
Packages/Premium Analytics/Widgets/TopPosts, mimicking the
"Top posts & pages" widget from #49568 as a self-contained,
mock-data-backed component.

The widget renders the most-viewed posts/pages as an overlay
leaderboard (each row links to the published content) and supports
previous-period comparison with colored deltas, mirroring the overlay
comparison mode of the toolkit's LeaderboardChart.

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

* Premium Analytics: reuse shared withChartTheme decorator in Top Posts story

Replace the duplicated ChartThemeProvider + withChartTheme in the Top Posts
widget story with the canonical decorator from the widgets-toolkit. Behavior
is identical.

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

* Premium Analytics: move widgets dir out of packages into the package root

Relocate widgets/ from premium-analytics/packages/widgets to
premium-analytics/widgets. Update the story's relative import to the toolkit
decorator, add widgets/**/* to the package tsconfig include, and register the
new path in the Storybook projects list so stories are still discovered.

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

* Storybook: changelog for premium-analytics widgets discovery path

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

* fix(premium-analytics): keep leaderboard overlay bar tint opaque

In overlay-label mode the bar should read as a faint tint of the
primary color so the label stays legible on top of it. We were
passing `hexToRgba( primary, 0.08 )` to the chart's `primaryColor`
prop, but that prop resolves through getElementStyles, which strips
the alpha channel — so the bar rendered at full primary color and the
intended fade never appeared.

Pre-blend the primary with white instead, producing the opaque
equivalent of an 8% alpha fill via charts' own `lightenHexColor`
(`normalizeColorToHex` first so non-hex theme colors are handled).
The result is opaque, so nothing gets stripped, and it still tracks
the themed primary. Top Posts and any other consumer of this shared
component inherit the fix automatically.

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

* Premium Analytics: fix Top Posts loading state and changelog entry

Address review feedback on the Top posts & pages widget:

- The Loading story rendered the empty state instead of loading. The
  toolkit LeaderboardChart short-circuits empty data to ChartEmptyState
  before the loading prop reaches the base chart, so loading+empty never
  showed a spinner. Render WidgetLoadingOverlay directly for the initial
  loading-with-no-data case (matching the sibling leaderboard widgets),
  and let the chart own the empty state via emptyStateText.
- Export WidgetLoadingOverlay from the widgets-toolkit public entry so
  consumers outside the toolkit can reuse it.
- Use a plain "&" in the changelog entry instead of "&amp;".

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Adam Wood <1017872+adamwoodnz@users.noreply.github.com>
@louwie17
louwie17 force-pushed the update/pa-introduce-customize-dashboard branch from 7ff7f71 to 22e10a7 Compare June 19, 2026 12:21
@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch 2 times, most recently from 57bebd8 to a348276 Compare June 19, 2026 13:26
@louwie17
louwie17 marked this pull request as ready for review June 19, 2026 13:41
@louwie17
louwie17 requested a review from a team June 19, 2026 13:41

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice work here @louwie17

I just noticed that @kangzj is adding a general layer against the same proxy.

So fetchReportTopPosts + normalizeTopPosts + the statsPath constant here largely re-implement what that PR provides, just scoped to one widget. Would it make sense to wait for #49777 to merge and converge on the shared helper?

Once #49777 lands, this widget could consume statsTopPostsQuery (wrapped in a thin hook) and drop its bespoke data layer.

Also, Darren's PR (#49505) provides a solid storybook foundation. It would be great to add a story leveraging it, but I'm also fine with making that a follow-up if you prefer.

@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch from a348276 to 0fee458 Compare June 23, 2026 09:29
@louwie17
louwie17 requested a review from a team as a code owner June 23, 2026 09:29
@louwie17
louwie17 requested review from chihsuan and kangzj June 24, 2026 13:37
@louwie17

Copy link
Copy Markdown
Contributor Author

@chihsuan or @retrofox this should be good for a re-review, I have addressed the changes.

Comment thread projects/packages/premium-analytics/widgets/top-posts/render.tsx Outdated
Comment thread projects/packages/premium-analytics/widgets/top-posts/render.tsx Outdated
Comment thread projects/packages/premium-analytics/widgets/top-posts/widget.ts
Comment thread projects/packages/premium-analytics/widgets/top-posts/widget.ts Outdated

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the updates @louwie17 this is looking good 👍

On Storybook: #49505 has landed, so the foundation is there, but I'm fine bringing the story in line with the other widgets as a follow-up rather than blocking this PR on it. 🙂

Two things left before merging:

  • Damian's feedback
  • My one nit on package.json — use link: for the @jetpack-premium-analytics/* deps

Other than these, LGTM. 🚢

Comment thread projects/packages/premium-analytics/widgets/top-posts/package.json Outdated
Comment thread projects/packages/premium-analytics/widgets/top-posts/render.tsx Outdated
louwie17 and others added 7 commits June 25, 2026 10:52
…ats hooks

Rebased onto trunk. The presentational TopPostsWidget, the stats proxy data
layer, and the designated useStatsTopPosts traffic hook have all since landed in
trunk, so this is reduced to the widget glue that connects them:

* widgets/top-posts/{widget.json,widget.ts,package.json} — register the
  `jpa/stats-top-posts` framed widget with range/num attributes.
* widgets/top-posts/render.tsx — fetch via the designated `useStatsTopPosts`
  hook, resolve the range preset to an absolute window, adapt the normalized
  report items to the `TopPostRow[]` the presentational widget expects, and
  filter by post type client-side.
* widgets/top-posts/__tests__/top-posts.test.tsx — render/empty/post-type tests
  driven through the stats proxy (apiFetch mocked).
* packages/data: re-export `computeDateRangeFromPreset` from the package root.
* tests: stub CSS imports so widget render tests can pull in widgets-toolkit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bmLMW4mTLBLuLtMaNvUca
The top-posts widget imports @jetpack-premium-analytics/widgets-toolkit,
which transitively imports @jetpack-premium-analytics/icons. These bare
specifiers resolve via Node module resolution to the link: symlinks in
node_modules, but the package only declared 7 of the 10 internal packages.
Add the missing link: entries (icons, init, widgets-toolkit) so the widget
builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz61RCPHjp2tot41TskF12
Re-apply lint fixes after rebase:
- Wrap the type-guard arrow in render.tsx's toTopPostRows (prettier/prettier).
- Order the @jetpack-premium-analytics/widgets-toolkit import before the
  @WordPress imports in top-posts-widget.tsx (import/order).

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

Address PR #49568 review feedback:

* Collapse the widget into render.tsx (matching the canonical hello-world
  reference). Remove top-posts-widget.tsx, index.ts and types.ts; rename the
  presentational TopPostsWidget component to TopPostsLeaderboard and keep it
  internal. A widget is the block-level abstraction (widget.json + widget.ts +
  render.tsx), so render.tsx is the component itself, like a block's edit.js.
* Stop exporting TopPostsAttributes / TopPostRow (used only here).
* Rename top-posts-widget.module.css -> top-posts.module.css.
* i18n: wrap widget.ts title and attribute labels in __().
* Replace the native anchor with the @wordpress/ui Link component
  (openInNewTab + unstyled variant to preserve the leaderboard row styling).
* Defer the Storybook story to the #49505 follow-up; drop its stale changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9fmCCXRxMgNf9whczP122
Use the host-provided `WidgetRenderProps<TopPostsAttributes>` from
@wordpress/widget-primitives for the widget's render entry point instead of a
hand-rolled props type — this matches the contract the widget dashboard invokes
render modules with (`ComponentType<WidgetRenderProps<unknown>>`). Add the
package to the widget's dependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9fmCCXRxMgNf9whczP122
* Re-add the prop-driven Top posts & pages story (stories/top-posts.stories.tsx)
  targeting the now-exported presentational `TopPostsLeaderboard`. Storybook has
  no analytics backend, so the data-connected entry point would only show empty
  chrome — driving the presentational layer with fixture rows is the only way to
  show populated, comparison, and long-label states.
* Export `TopPostsLeaderboard` and the `TopPostRow` type for the story.
* Inline the small `TopPostLabel` component into `buildLeaderboardData`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9fmCCXRxMgNf9whczP122
- package.json: use link: for internal @jetpack-premium-analytics/* deps
  (they are not pnpm workspace members)
- widget.ts: drop redundant `presentation` (owned by widget.json); add the
  `postType` attribute so the widget redefinition is complete
- render.tsx: rename the `name` attribute to `postType`; treat empty as "all
  types"; collapse the duplicated TopPostsLeaderboard JSDoc to @PARAM {Type}

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AuSeh4kAoQvoKeXQbV2SLw
@louwie17
louwie17 force-pushed the wooa7s-1489-stats-top-posts branch from 082c9ef to 44f1ea3 Compare June 25, 2026 09:08
@louwie17

Copy link
Copy Markdown
Contributor Author

@chihsuan & @retrofox this is ready for a re-review, thanks for the detailed reviews :)

@louwie17
louwie17 requested review from chihsuan and retrofox June 25, 2026 10:16

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

Approving since the implementation looks sound and the widget also works well. Two non-blocking notes from review:

  1. The user-facing naming is a little inconsistent: the code, changelog, and behavior describe this as Top posts & pages, but widget.ts/widget.json title it Top pages by views. Since the default behavior includes both posts and pages, I think we should align the product copy before/after merge, e.g. Top posts & pages by views if that is the intended scope.

  2. The postType filter is applied client-side after requesting max: num from the mixed top-posts endpoint. That means filtering to pages/posts can show fewer than the configured number even if more matching rows exist below the mixed top-N cutoff. That may be fine for this first pass, but it is worth tracking if the filter is meant to guarantee N results of the selected type.

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM ✅ Thanks for the thorough iteration here.

@louwie17
louwie17 merged commit 04d0aff into trunk Jun 26, 2026
98 checks passed
@louwie17
louwie17 deleted the wooa7s-1489-stats-top-posts branch June 26, 2026 06:55
@louwie17

Copy link
Copy Markdown
Contributor Author

The user-facing naming is a little inconsistent: the code, changelog, and behavior describe this as Top posts & pages, but widget.ts/widget.json title it Top pages by views. Since the default behavior includes both posts and pages, I think we should align the product copy before/after merge, e.g. Top posts & pages by views if that is the intended scope.

I took the name from Eder's demo, but I guess there the dropdown highlights it!

Screenshot 2026-06-26 at 08 56 59

The postType filter is applied client-side after requesting max: num from the mixed top-posts endpoint. That means filtering to pages/posts can show fewer than the configured number even if more matching rows exist below the mixed top-N cutoff. That may be fine for this first pass, but it is worth tracking if the filter is meant to guarantee N results of the selected type.

Good catch, I will fix this in a follow up.

@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. [Status] In Progress labels Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] Premium Analytics [Plugin] 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.

4 participants