Skip to content

Premium Analytics: Add email post detail views - #50545

Merged
dognose24 merged 14 commits into
trunkfrom
codex/wooa7s-1623-email-detail
Jul 16, 2026
Merged

Premium Analytics: Add email post detail views#50545
dognose24 merged 14 commits into
trunkfrom
codex/wooa7s-1623-email-detail

Conversation

@dognose24

@dognose24 dognose24 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Part of WOOA7S-1623.

Proposed changes

  • Add fixed Email opens and Email clicks compositions to the post detail page.
  • Reuse the existing email highlights and breakdown widgets for opens, clicks, devices, clients, countries, and top links.
  • Combine the opens and clicks summaries for the Email clicks highlights.
  • Add the country map variant used by the wide Location clicks card while keeping the adjacent leaderboard capped.
  • Gate the email tabs on the post's send summary: they only show when total_sends from the per-post opens rate summary is positive — the same query the Email top row reads, so React Query shares the result and the gate costs no extra request. It fails closed (hidden while loading/errored), and deep links to a gated tab fall back through the existing hidden-tab path. Calypso infers the same availability from subscription settings, post metadata, and a 2023-05-30 date cutoff because it decides before fetching; the send summary is the direct signal.
  • Align the cards with the design spec: the highlights row is titled Newsletter performance with Sent / Total unique opens / Total opens|clicks / Open|Click rate tiles, and each breakdown card carries its design title (Location/Platforms/Clients opens|clicks, Top links) via page-local aliases of jpa/email-breakdown — the host titles a card by widget type, so the fixed page clones the resolved type (render module and all) under variant names that can never leak into a widget gallery (this page has none).
  • Drop the header dropdowns: the top row's Opens/Clicks switch duplicates the tab split and the breakdown's view selector would fight the fixed composition, so both attributes return to default (low) relevance.
  • Email clicks lays out as 3/1 + 2/2 (mapped Location clicks + Platforms, then Clients + Top links).
  • Bound the breakdown list height so every view scrolls inside its card (the non-map wrapper's flex sizing was inert inside WidgetState's plain-block ready container, so overflow was clipped instead of scrolling).
  • Hold the hidden-tab URL normalization until the email gate's send summary settles, so a deep link to an email tab survives the first load instead of being rewritten to Post traffic (review follow-up).
  • Split out per review: the rate-fraction fix moved to Premium Analytics: Email top row rates are 0–1 fractions, not percentages #50563 and the date-filters relocation to Premium Analytics: move the post detail date filters below the tab bar #50562; this PR no longer contains either.
  • Keep the performance chart out of scope for this PR (tracked in WOOA7S-1738).

Related product discussion/links

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

No. It presents existing email Stats API data in the post detail page.

Testing instructions

  • Open Premium Analytics and navigate to a post detail page with newsletter statistics.
  • On a post never emailed to subscribers (or a site without newsletter), the Email opens/clicks tabs stay hidden and a deep link to them falls back to Post traffic.
  • Select Email opens and confirm the highlights row is followed by the countries/devices/clients cards in a 2/1/1 layout.
  • Select Email clicks and confirm the highlights show Sent, Total unique opens, Total clicks, and Click rate.
  • Confirm Email clicks shows the wide Location clicks card with a country map, Platforms clicks, Clients clicks, and Top links.
  • Confirm Top links renders internal link types and safe user-content links.
  • Confirm no card shows a header dropdown (the Opens/Clicks split lives in the tabs), each breakdown card carries its design title, and lists longer than a card scroll inside it (the Location card scrolls its list while the map stays put).
  • Run pnpm test -- --runInBand and pnpm run typecheck from projects/packages/premium-analytics.
  • Run pnpm run build from projects/packages/premium-analytics.

Screenshots

Email Opens

截圖 2026-07-16 上午10 02 42

Email Clicks

截圖 2026-07-16 上午10 02 53 截圖 2026-07-16 上午10 03 06

@dognose24 dognose24 added the Enhancement Changes to an existing feature — removing, adding, or changing parts of it label Jul 15, 2026
@dognose24 dognose24 self-assigned this Jul 15, 2026
@dognose24 dognose24 added the Enhancement Changes to an existing feature — removing, adding, or changing parts of it label Jul 15, 2026
@github-actions

github-actions Bot commented Jul 15, 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!

@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 15, 2026
dognose24 and others added 2 commits July 15, 2026 23:47
# Conflicts:
#	projects/packages/premium-analytics/routes/post-detail/config/tab-layouts.test.ts
The email tabs only show for posts actually sent to subscribers:
total_sends from the per-post opens rate summary is the send signal
(the same query the Email top row reads, so React Query shares the
result). Calypso infers availability from subscription settings and
post metadata because it decides before fetching; the summary is the
direct source. Fails closed while loading or errored, and deep links
to a gated tab fall back through the existing hidden-tab path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jp-launch-control

jp-launch-control Bot commented Jul 15, 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

dognose24 and others added 3 commits July 16, 2026 00:13
- Fix the rate tiles: the endpoint returns 0-1 fractions (Calypso
  multiplies by 100), so stop dividing by 100 - 2 sends / 2 opens now
  reads 100%, not 1%.
- Design strings: the top row becomes 'Newsletter performance' with
  Sent / Total unique opens / Total opens|clicks / Open|Click rate.
- Drop the header dropdowns: the top row's metric switch duplicates the
  tab split and the breakdown's view selector fights the fixed
  composition, so both attributes return to default (low) relevance.
- Per-card design titles: the host titles a card by widget type, so the
  page clones jpa/email-breakdown into page-local aliases (Location/
  Platforms/Clients opens|clicks, Top links) that reuse the resolved
  render module.
- Email clicks grid becomes 3/1 + 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The non-map row wrapper sat inside WidgetState's plain-block ready
container, so its flex sizing was inert and the row grew to content
height - overflow was clipped by the root instead of scrolling. Give it
the same definite block-size the map variant already had, so the
leaderboard scrolls internally in every view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dognose24
dognose24 marked this pull request as ready for review July 15, 2026 17:27
@dognose24
dognose24 requested review from a team as code owners July 15, 2026 17:27
@dognose24 dognose24 added [Status] Needs Review This PR is ready for review. [Tests] Includes Tests [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. and removed [Status] In Progress [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. labels Jul 15, 2026
dognose24 and others added 2 commits July 16, 2026 09:51
The filters now sit directly under the tabs, mirroring the main
dashboard's placement, with the summary header on its own row below.
Restores SectionTabs' default bottom margin (the header row no longer
owns that spacing) and drops the header's since-obsolete side-by-side
responsive layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16px top padding stacks with the filters row's 16px bottom padding to
32px above the heading; 32px below mirrors it.

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

@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 this @dognose24

This PR seems cover a few separate concerns: the rate fix, reusable Email widget enhancements, Post Details layout changes, and the final tab integration.

Would you be open to splitting these into smaller PRs? I think it would make the changes easier to review and reduce the risk of unrelated regressions. 🙏

export function usePostDetailTabs() {
export function usePostDetailTabs( postId: number ) {
const opens = useStatsEmailOpensBreakdown( postId, 'rate', { enabled: postId > 0 } );
const summary = ( opens.data as StatsEmailBreakdown | undefined )?.summary;

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.

While this query is loading, data is undefined, so an email-opens deep link gets replaced with post-traffic before we know whether Email stats exist. Should we wait for the query to settle before normalizing the URL?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — this was real: the normalization effect fired during the first load and rewrote a legitimate email-opens deep link before the gate knew whether email stats exist.

Fixed in d2e94a2: the hidden-tab URL normalization now waits for the send summary to settle (isLoading is false while the query is disabled and once it resolves or errors, so only the first load holds it off). The visible fallback still renders immediately — only the URL write waits. Added a regression test for the loading + deep-link case.

dognose24 and others added 2 commits July 16, 2026 11:07
- Revert the email rate fraction fix here; it moves to its own PR
  (#50563) per review.
- Hold the hidden-tab URL normalization until the email gate's send
  summary settles: rewriting while it loaded bounced a legitimate
  email-tab deep link to post-traffic before we knew whether email
  stats exist. The visible fallback still renders immediately.

The date-filters relocation was likewise split out to #50562 (reverted
in the two commits before this one).

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

Copy link
Copy Markdown
Contributor Author

Thanks for the review @chihsuan! Split done where the pieces were genuinely independent:

I kept the email widget enhancements and the tab integration together: the fixed compositions directly reference the new widget surface (the showMap attribute, the merged Clicks summary, and the page-local title aliases), so splitting them would leave an intermediate state that can't be exercised or reviewed on its own — the enhancements only become observable once the layouts place them. Happy to split further if you still prefer it after this trim.

Also fixed the deep-link race you flagged inline — good catch. The URL normalization now waits for the send summary to settle.

@dognose24
dognose24 requested a review from chihsuan July 16, 2026 03:30

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

Thorough review of the package (full-file reads, JS tests, typecheck). Findings from the review of cac0788:

  • [blocker — already fixed in d2e94a2] Deep links / refreshes on an email tab bounced to Post traffic: the hidden-tab URL normalization raced the send-summary gate query and always won on a cold load. The gateSettled = ! opens.isLoading guard resolves it, and I verified the disabled-query edge: on @tanstack/react-query v5, a disabled query (no post scope) reports isLoading: false, so normalization still runs there. The added regression test covers the loading deep-link case. ✅
  • [moot] A stale test name (“converts the 0–100 rate”) flagged against the 0–1 fraction change no longer applies after the rate handling was split out and reverted in d2e94a2.
  • [suggestion] One minor note posted inline: the hidden country map still mounts Google Charts below 720px.

Also verified: changelog entry valid; i18n domains correct; new CSS uses logical properties with a proper container-type ancestor; jpa/email-breakdown / jpa/email-top-row have no consumers outside the post detail page, so the relevance removals and layout wrapper are safe; package tests (32 PR-scoped) and typecheck pass locally. The one unrelated local test failure (WordAds window clamp in packages/data) is byte-identical to trunk and green in CI.

LGTM once the inline note is considered — nothing blocking remains.

showLegend={ false }
dataFormat={ DATA_FORMAT }
/>
{ renderMap && (

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.

[suggestion] Below a 720px container the map is display: none (container query in style.module.css), but GeoChart still mounts here, so narrow layouts pay the Google Charts load for a chart that's never visible. Fine to defer — a JS-side width check or lazy-mount on visibility would avoid the dead weight.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Went ahead and implemented it in 84b8a01 rather than deferring — GeoChart now mounts only once the measured container is ≥ the same 720px the CSS query needs to show it (ResizeObserver on the widget root, first paint defaults to unmounted), so a narrow layout never loads Google Charts at all. CSS still owns the visual fallback.

kangzj
kangzj previously approved these changes Jul 16, 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.

Approving — the deep-link race flagged earlier is fixed in d2e94a2 and nothing blocking remains (full findings in my review above). The PR is on the larger side, but given the deadline I'm happy for it to merge as is; the one remaining inline note (lazy-mounting the hidden map) can land as a follow-up.

chihsuan
chihsuan previously approved these changes Jul 16, 2026

@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 thoughtful update! @dognose24

I left one concern about the email-tab gate treating a failed request as a completed availability check but it's minor.

The rest of the data merging and shared-query approach looks sensible to me. 🙏

Pre-approving.

const hasEmailStats = Number( summary?.total_sends ?? 0 ) > 0;
// `isLoading` is false while the query is disabled (no post scope) and once
// it settles (data or error), so this only holds off during the first load.
const gateSettled = ! opens.isLoading;

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.

One small concern: if this request fails or times out, isLoading becomes false while the data is still unavailable. We would then hide the Email tabs and rewrite an Email deep link to Post traffic, even though we don’t know whether the post has email stats yet.

Could we preserve the requested section on error and only normalize the URL after a successful response?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 84b8a01 — normalization now requires the query to have succeeded (opens.isSuccess), not merely settled: on a failed/timed-out request the URL keeps the requested email section (the tabs stay hidden, fail-closed, and a later successful refetch settles it). Non-email deep links don't depend on the gate and normalize right away. Added a regression test for the errored deep-link case.

dognose24 added a commit that referenced this pull request Jul 16, 2026
…ages (#50563)

The per-post stats/<opens|clicks>/emails/<id>/rate summary reports
rates as 0-1 fractions (wp-calypso renders rate * 100), but the widget
divided by 100 again - 2 sends / 2 opens rendered as 1% instead of
100%. Pass the fraction straight to the percentage formatter, and align
the test and Storybook fixtures with the real payload shape.

Split out of #50545 per review.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- An email deep link is only normalized after the send-summary query
  succeeds: a failed/timed-out request can't tell us whether the post
  has email stats, so the URL keeps the requested section (tabs stay
  hidden, a later successful refetch settles it). Non-email deep links
  don't depend on the gate and normalize right away.
- Mount GeoChart only when the measured container is at least the 720px
  the CSS container query needs to show it, so narrow layouts never pay
  the Google Charts load for an invisible map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dognose24
dognose24 dismissed stale reviews from chihsuan and kangzj via 84b8a01 July 16, 2026 08:46
…ail-detail

# Conflicts:
#	projects/packages/premium-analytics/widgets/email-top-row/__tests__/email-top-row.test.tsx
dognose24 added a commit that referenced this pull request Jul 16, 2026
#50562)

* Post detail: move the date filters below the tab bar

The filters now sit directly under the tabs, mirroring the main
dashboard's placement, with the summary header on its own row below
(16px + 16px stacking to a symmetric 32px around the heading). Restores
SectionTabs' default bottom margin and drops the header's since-obsolete
side-by-side responsive layout.

Split out of #50545 per review — this affects the whole post detail
page, not only the email tabs.

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

* Address review: use the gap-2xl token for the header's bottom padding

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@dognose24
dognose24 merged commit cad6784 into trunk Jul 16, 2026
73 checks passed
@dognose24
dognose24 deleted the codex/wooa7s-1623-email-detail branch July 16, 2026 13:44
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Jul 16, 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 [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.

3 participants