Premium Analytics: add Top posts & pages stats widget - #49568
Conversation
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
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:
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. |
Code Coverage SummaryThis 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. 🤷 |
909996e to
d060b2d
Compare
f1a7fee to
2899350
Compare
d060b2d to
5bb6dd9
Compare
13834c1 to
df71ef4
Compare
5bb6dd9 to
7ff7f71
Compare
df71ef4 to
a8ec740
Compare
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: 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 "&". 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>
7ff7f71 to
22e10a7
Compare
57bebd8 to
a348276
Compare
There was a problem hiding this comment.
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.
a348276 to
0fee458
Compare
chihsuan
left a comment
There was a problem hiding this comment.
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. 🚢
…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
082c9ef to
44f1ea3
Compare
There was a problem hiding this comment.
Approving since the implementation looks sound and the widget also works well. Two non-blocking notes from review:
-
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.
-
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
left a comment
There was a problem hiding this comment.
LGTM ✅ Thanks for the thorough iteration here.

Fixes WOOA7S-1489
Proposed changes
Adds the
jpa/stats-top-postsdashboard widget to Premium Analytics, the kickoff Stats card port. Rebased ontotrunk: the presentationalTopPostsWidget, the stats proxy data layer, and the designateduseStatsTopPoststraffic hook/query have all since landed intrunk, so this PR is now just the widget glue that connects them.widgets/top-posts/— registers thejpa/stats-top-postswidget (framedpresentation) withrange/numattributes: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 designateduseStatsTopPostsStats traffic hook, resolves therangepreset to an absolute window viacomputeDateRangeFromPreset, adapts the normalized report items ({ label, views, link, type }) to theTopPostRow[]the presentationalTopPostsWidgetexpects, and filters by post type client-side. Runs insideWidgetRootfor 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-fetchmocked.packages/data— re-exportscomputeDateRangeFromPresetfrom the package root so the widget can resolve range presets.tests/— adds astyle-stub.cjs+moduleNameMapperentry so widget render tests can pull inwidgets-toolkit(whose transitive@automattic/ui/@automattic/chartsCSS imports are otherwise skipped by jest'stransformIgnorePatternsand parsed as JS).The widget's
num(result count) maps to the WPCOM stats API'smax; 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/v1stats proxy via the designateduseStatsTopPostshook.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: