Skip to content

Premium Analytics: add post/page detail traffic view#50096

Open
kangzj wants to merge 4 commits into
trunkfrom
add/WOOA7S-1622-post-detail-page
Open

Premium Analytics: add post/page detail traffic view#50096
kangzj wants to merge 4 commits into
trunkfrom
add/WOOA7S-1622-post-detail-page

Conversation

@kangzj

@kangzj kangzj commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Part of WOOA7S-1622

Why

Premium Analytics could only show site-wide stats. There was no way to drill into a single post or page. This adds a dedicated detail page for one post/page — its own traffic, email opens, and email clicks — so authors can see how an individual piece of content performs without leaving the analytics dashboard.

Proposed changes

  • Add a new /post/$postId SPA route with its own header — a Stats / <post title> breadcrumb (linking back to the dashboard) and a summary card (type badge, title, published date, featured image).
  • Add three tabs — Post traffic, Email opens, Email clicks — each backed by an independent, customizable per-tab widget grid, deep-linkable via ?section=.
  • Reuse the dashboard's date-range + "Compare to" comparison picker and its widget/grid customization; the page mirrors the dashboard while working independently.
  • Extract the dashboard's date-filter controller into a shared useReportDateFilters hook in the routing package and consume it from both the dashboard and the new page, instead of duplicating the staged-search logic.
  • Scope the page to a single resource: seed post_id into the URL, carry it through ReportParams/normalizeReportParams, and resolve widget report params from the current matched route (so widgets pick up the date range and post_id on any route, not only the dashboard at /).

Out of scope (tracked separately under WOOA7S-1458): the post-scoped Performance / Latest comments / Latest likes / UTM widgets shown in the design. This PR ships the page framework; those widgets land in follow-ups and drop straight into the tabs.

Screenshots

Captured on a local docker env. It runs in offline mode (no live WordPress.com data), so widgets are empty and the title is blank; the post-scoped widgets and their data arrive in follow-ups.

Post detail page — the Stats breadcrumb, the three tabs, the post summary card, the shared date-range + "Compare to" picker, and the customizable widget grid.

post detail page

Widget customization — the "Add widget" inserter on the page.

add widget inserter

Verification

Built and exercised end-to-end in a local WordPress env (?page=jetpack-premium-analytics-wp-admin&p=/post/<id>):

Manual browser testing
- Route loads at /post/$postId (SPA path via ?p=); guards mirror the dashboard.
- On first load the URL is seeded with the date range and post_id="<id>".
- Header renders: "Stats" breadcrumb (links to the dashboard) + the post summary card.
- Tabs render "Post traffic / Email opens / Email clicks"; switching updates ?section=
  and each tab keeps its own widget layout.
- Date range ("Last 30 days") and "Compare to" picker render and drive the page.
- "Add widget" inserter opens and lists the registered widgets (widgetModule entity
  resolves on a direct deep link, not only via the dashboard); "Layout settings" works.
- No console errors on the detail page.
- Regression check: the main dashboard renders unchanged after the shared-hook refactor
  and the WidgetRoot search-resolution change.
Automated checks
$ pnpm run typecheck   # tsgo --noEmit — clean
$ pnpm run test        # config/tabs + normalize-report-params (incl. post_id) + stats-queries — pass
$ pnpm run build       # all routes (incl. post-detail) + widgets build; registry regenerated
$ pnpm eslint <changed files>  # clean

Related product discussion/links

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

No. It surfaces existing Stats data scoped to a single post/page; no new data is tracked or collected.

Testing instructions

  • Build the package: jetpack build packages/premium-analytics (or pnpm run build inside projects/packages/premium-analytics), then build the plugin: jetpack build plugins/premium-analytics.
  • In wp-admin, open Premium Analytics, then navigate to a post detail via admin.php?page=jetpack-premium-analytics-wp-admin&p=/post/<POST_ID> (use a published post/page ID).
  • Verify against the issue's acceptance criteria:
    • The page loads at its own route for an individual post/page.
    • The header shows Stats / <post title> (breadcrumb) plus a summary card (type, title, published date, featured image). "Stats" links back to the dashboard.
    • Three tabs are present — Post traffic, Email opens, Email clicks — and switching between them updates the URL and preserves each tab's own widget layout.
    • The page mirrors the dashboard's date-range + "Compare to" picker and is customizable (Add widget / Layout settings), but works independently of the main dashboard.
    • The page and its widgets are scoped to the single post (post_id present in the URL/report params).
  • Confirm the main dashboard (&p=/) still renders and behaves as before.

kangzj added 2 commits July 1, 2026 12:52
Add the single-resource detail page for an individual post or page: a new
/post/$postId route with its own header (Stats breadcrumb + summary card),
three tabs (Post traffic / Email opens / Email clicks), the shared date-range
and comparison picker, and a customizable per-tab widget grid — mirroring the
dashboard while working independently and scoped to one resource.

- Extract the dashboard's date-filter controller into a shared
  useReportDateFilters hook in the routing package and reuse it in both the
  dashboard and the new page instead of duplicating the staged-search logic.
- Seed post_id into the URL search and resolve the current route's search in
  WidgetRoot so widgets on any route (not only the dashboard at /) receive the
  date range and single-resource scope.
…dget search per-route

Address local review findings on the single-resource scoping:

- Add post_id to ReportParams and preserve it in normalizeReportParams so the
  seeded scope actually reaches widgets via reportParams (it was previously
  stripped by the normalizer). Typed string|number so the URL string and the
  existing numeric query-layer usage (statsUtmQuery) both hold; covered by tests.
- Resolve WidgetRoot report params from the current matched route
  (useSearch strict:false) instead of a hardcoded '/', so widgets pinned to
  options.from='/' also pick up the detail page's date range and post_id.
  options.from is retained but documented as ignored.
@kangzj kangzj requested review from a team as code owners July 1, 2026 01:15
@kangzj kangzj added [Package] Premium Analytics [Plugin] Premium Analytics Enhancement Changes to an existing feature — removing, adding, or changing parts of it labels Jul 1, 2026
@github-actions github-actions Bot added the Docs label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 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.

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jul 1, 2026
@jp-launch-control

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

Replace non-existent design-system tokens flagged by stylelint
(plugin-wpds/no-unknown-ds-tokens): --wpds-color-content-neutral-medium/strong
→ --wpds-color-fg-content-neutral-weak/neutral, and --wpds-dimension-radius-lg/md
→ --wpds-border-radius-md/sm.

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

Adds a new Premium Analytics SPA route for post/page detail analytics, reusing the dashboard’s date-range + comparison UX and widget customization while scoping widgets via post_id in URL/report params.

Changes:

  • Introduce a new /post/$postId route with header breadcrumbs, post summary card, and three section tabs (traffic / email opens / email clicks).
  • Extract the dashboard date-filter controller into a shared useReportDateFilters hook and consume it from both dashboard and post detail pages.
  • Update widget report-param resolution to read search params from the currently matched route (not just /) and preserve post_id through report-param normalization.

Reviewed changes

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

Show a summary per file
File Description
projects/packages/premium-analytics/routes/post-detail/style-imports.d.ts Adds TS module declarations for CSS/SCSS imports used by the new route.
projects/packages/premium-analytics/routes/post-detail/stage.tsx Implements the post-detail stage UI (tabs, summary, date filters, widget grid).
projects/packages/premium-analytics/routes/post-detail/stage.module.scss Styles layout spacing/padding for the post-detail page.
projects/packages/premium-analytics/routes/post-detail/route.ts Adds route guards + initial URL seeding (dates + post_id) and widgetModule entity bootstrap.
projects/packages/premium-analytics/routes/post-detail/package.json Declares the new route package, dependencies, and route mount path.
projects/packages/premium-analytics/routes/post-detail/hooks/use-post-summary.ts Fetches Stats post info + core post entity data for the header summary card.
projects/packages/premium-analytics/routes/post-detail/hooks/use-post-detail-tab-layout.ts Persists per-tab widget layouts for the post-detail page via preferences.
projects/packages/premium-analytics/routes/post-detail/hooks/use-active-tab.ts Drives active tab via ?section= using staged search.
projects/packages/premium-analytics/routes/post-detail/hooks/index.ts Exports post-detail hooks.
projects/packages/premium-analytics/routes/post-detail/hooks/constants.ts Defines preferences scope/key for post-detail layouts.
projects/packages/premium-analytics/routes/post-detail/config/tabs.ts Defines post-detail tab IDs and translated labels.
projects/packages/premium-analytics/routes/post-detail/config/tabs.test.ts Adds unit tests for tab definitions and URL resolution behavior.
projects/packages/premium-analytics/routes/post-detail/config/tab-layouts.ts Adds runtime validator for persisted tab layout map shape.
projects/packages/premium-analytics/routes/post-detail/config/index.ts Re-exports post-detail config helpers/types.
projects/packages/premium-analytics/routes/post-detail/components/stats-breadcrumbs/stats-breadcrumbs.tsx Renders “Stats / ” breadcrumb header UI.
projects/packages/premium-analytics/routes/post-detail/components/stats-breadcrumbs/stats-breadcrumbs.module.scss Styles breadcrumbs.
projects/packages/premium-analytics/routes/post-detail/components/post-summary-card/post-summary-card.tsx Renders post/page summary card (type, title, publish date, featured image).
projects/packages/premium-analytics/routes/post-detail/components/post-summary-card/post-summary-card.module.scss Styles the post summary card.
projects/packages/premium-analytics/routes/post-detail/components/post-detail-tabs/post-detail-tabs.tsx Presentational wrapper for the tab bar and shared Tabs.Root.
projects/packages/premium-analytics/routes/post-detail/components/post-detail-tabs/post-detail-tabs.module.scss Styles the tab bar and active-state colors.
projects/packages/premium-analytics/routes/post-detail/components/index.ts Exports post-detail components.
projects/packages/premium-analytics/routes/dashboard/stage.tsx Refactors dashboard to use useReportDateFilters instead of inlined staged-search logic.
projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx Resolves report params from the current matched route search (strict:false) and deprecates options.from.
projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md Documents options.from as deprecated/ignored.
projects/packages/premium-analytics/packages/routing/src/index.ts Exports useReportDateFilters from routing package entrypoint.
projects/packages/premium-analytics/packages/routing/src/hooks/use-report-date-filters/use-report-date-filters.tsx Adds shared date filter controller hook extracted from the dashboard.
projects/packages/premium-analytics/packages/routing/src/hooks/use-report-date-filters/index.ts Barrel export for useReportDateFilters.
projects/packages/premium-analytics/packages/routing/src/hooks/index.ts Re-exports useReportDateFilters alongside existing hooks.
projects/packages/premium-analytics/packages/routing/package.json Adds dependencies needed by the new routing hook (core-data/data/date-fns/react).
projects/packages/premium-analytics/packages/data/src/utils/search.ts Extends ReportParams and normalization to preserve post_id.
projects/packages/premium-analytics/packages/data/src/utils/tests/normalize-report-params.test.ts Adds tests ensuring post_id survives normalization.
projects/packages/premium-analytics/changelog/add-WOOA7S-1622-post-detail-page Adds changelog entry for the new post detail page feature.

Comment thread projects/packages/premium-analytics/packages/data/src/utils/search.ts Outdated
Comment thread projects/packages/premium-analytics/routes/post-detail/route.ts Outdated
@kangzj kangzj marked this pull request as draft July 1, 2026 02:32
- Validate post_id is a positive integer both at the route seed (so /post/foo
  never puts a malformed post_id in the URL) and in normalizeReportParams
  (defense for hand-edited deep links), coercing it to a number and dropping
  non-numeric/zero/negative values (comments #3502774220, #3502774234).
- Scope usePostSummary's Stats query to the 'post' field since the header only
  needs title/type (comment #3502774246).
@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kangzj's task in 1m 8s —— View job


Re-review — post_id validation & summary query

Re-reviewed commit 08c3b5e0. All three items from the earlier round are addressed correctly. ✅

  • post_id validation in normalizeReportParams
  • post_id seeding guard in route.ts
  • usePostSummary fields: ['post']
  • Test coverage for the new validation

1. normalizeReportParams coercion — ✅

search.ts:118-133 coerces via Number() + Number.isInteger(n) && n > 0, then only spreads post_id when valid. This correctly drops every malformed case:

  • 'foo'NaN → dropped
  • '0' / '-5' → not > 0 → dropped
  • '12.5'Number.isInteger(12.5) is false → dropped
  • undefined/absent → Number(undefined) = NaN → dropped

Defense-in-depth for hand-edited deep links is the right call, since WidgetRoot reads straight from URL search now.

2. Route seed guard — ✅

route.ts:46-49 gates the seed on /^\d+$/.test(...) && Number(...) > 0. /post/foo no longer seeds post_id, and preserving the raw path param in the redirect (route.ts:83) keeps the redirect on-route — good nuance, and the comment explains why.

3. usePostSummary field scoping — ✅

use-post-summary.ts:47 passes fields: [ 'post' ]. Verified StatsPostParams.fields?: StatsPostField[] exists and 'post' is a valid StatsPostField (stats-post-query.ts:9-22), so this is type-safe and correctly trims the payload to just the title/type the header uses.

Tests — ✅

normalize-report-params.test.ts:296-323 covers the valid case ('2428'2428), the absent case, and a parametrized it.each over ['foo','0','-5','12.5']. Good coverage of the boundary conditions.


Minor (non-blocking) observations

  • Type comment vs. behavior: ReportParams.post_id?: string | number is annotated "String from the URL; numeric at the query layer" (search.ts:51). After this change normalizeReportParams output is always numeric, while the route seeds the URL as a string — both true, so the union type is still justified. Re your own thread comment: keeping it on ReportParams is reasonable given it must flow through WidgetRoot → every widget's reportParams; a split type wouldn't traverse that path. No change needed.
  • WidgetRoot options.from: now destructured out entirely (widget-root.tsx), retained in the props type but documented as ignored. Backward-compatible; unused-prop is fine for TS. 👍

Nothing blocking from my side — the validation, query scoping, and tests all look correct and cohesive.

@kangzj

kangzj commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Review-cycle summary — 4d0d78f5b408c3b5e05c

1 round; CI green; 4 threads resolved; Copilot + claude addressed. No blocking feedback remains.

What changed during the cycle

Commits added:

  • 08c3b5e05c — Address review: validate post_id scope and slim the summary query

Diff summary: 4 files changed, 32 insertions(+), 8 deletions(-)

Review threads addressed:

Source Comment Resolution
Copilot normalizeReportParams doesn't validate post_id Coerce to a positive integer, drop non-numeric/zero/negative (08c3b5e)
Copilot route.ts seeds post_id without validation Only seed when the path param is a positive integer; /post/foo seeds nothing (08c3b5e)
Copilot usePostSummary fetches full payload Scope the Stats query to fields: [ 'post' ] (08c3b5e)
kangzj (owner) Should post_id be a separate type? Kept on ReportParams (a URL param that must flow through WidgetRoot), now validated; rationale in-thread

CI: all required checks passing (52 passed, 0 failed).

@kangzj kangzj added [Status] Needs Team Review Obsolete. Use Needs Review instead. and removed [Status] In Progress labels Jul 1, 2026
@kangzj kangzj marked this pull request as ready for review July 1, 2026 02:48
@kangzj kangzj removed the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jul 1, 2026
@kangzj kangzj requested a review from retrofox July 1, 2026 04:04
@kangzj kangzj self-assigned this Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docs Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] Premium Analytics [Plugin] Premium Analytics [Status] In Progress [Status] Needs Team Review Obsolete. Use Needs Review instead.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants