Stats: modernize the VideoPress video details page - #112499
Conversation
Move the videodetails module out of the summary switch into its own stats-video-detail page component, keeping the existing URL. Adds a video details header card (title, published date, thumbnail linking to the media library) and the shared stats breadcrumb trail. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the old single-metric StatsVideoSummary with a VideoSummary section modeled on stats-post-summary: a date label with previous/next arrows and a Days/Weeks/Months/Years segmented control driving the statsVideo query. Daily data is paged 30 bars at a time like the post chart. Removes the now-unused stats-video-summary component. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fetch all four statsVideo series (views, impressions, watch time, retention rate) and render them as selectable cards below the chart, matching the post details layout from the JA designs. Card totals are computed over the dates visible in the chart; retention rate uses a views-weighted average. The chart's statType now lives in page state, seeded from the statType query param so existing links keep working. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the legacy StatModuleVideoDetails list with an Embedded pages card matching the rest of the redesigned video details page. Pages still display as URLs since the stats/video endpoint only returns embed URLs; showing post titles needs the API to return post IDs/titles. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Jetpack Cloud Live (direct link)
Automattic for Agencies Live (direct link)
Dashboard Live (dotcom) (direct link)
|
The stats/video endpoint only returns buckets from the video's first activity onwards, so a young video yields one or two bars, which the is-chart-tabs layout (max-width bars + space-between) then scatters away from the axis labels. Fill gaps between buckets, extend the day view backwards to a full 30-bar window per the designs, and add the has-less-than-three-bars fallback class used by the traffic chart. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A backend investigation confirmed stats/video/:id treats period as a fixed trailing-window selector (day yields only 2 daily buckets, year yields ~13 monthly buckets) rather than a bucket granularity, and that its statType only recognizes watch_time and impressions — views falls back to the plays column and there is no retention series at all. Rework the summary accordingly: fetch the 30-day daily window for the Days/Weeks views and the 365-day monthly window for Months/Years, and aggregate buckets client-side. Views now covers the same 30-day window and plays metric as the Traffic Videos module and the All videos page. Retention rate is derived per bucket and in total with the canonical complete_stats formula, (watch time / plays) / video duration, using the media item duration — replacing the bogus fourth query whose response was silently the plays series. Also harden the statsVideo normalizer against the endpoint's empty-window response shape, and drop the now-unneeded zero-fill and paging logic. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stats/video endpoint's month window spans 31 days inclusive, one more than the 30-day window the Videos module and All videos page show, so the Views total was off by the oldest day's plays. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chart's x-axis labels have a fixed 42px width, so 'MMM YYYY' wraps and collides with the metric cards below. Show 'MMM' like the legacy year view did — the full month and year remain in the bar tooltip and the date heading — and key bar selection by startDate since bare month labels can repeat within a 13-month window. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The date heading (stats-date-label) renders weeks Monday-Sunday, while the weekly aggregation grouped by the locale's week start (Sunday in en), so a selected bar's tooltip range was one day off from the heading. Group by isoWeek, which matches the heading's convention on every Monday key regardless of locale. Also center capped chart bars in their slots (space-around) so bars line up with the mathematically positioned x-axis labels when there are only a few buckets. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the 'MMM YYYY' month labels — the post details chart uses the same format and its second wrapped line is absorbed by the spacing below the chart, so match that spacing above the metric cards instead of shortening the label. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Card base carries margin: 0 auto; inside the page's flex column the auto inline margins shrink-to-fit and center the card instead of letting align-items: stretch apply, so the header and embeds cards rendered narrower than the chart section between them. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recordCurrentScreen silently ignores screens missing from possibleBackLinks, and videodetails was never registered (a gap dating back to the summary-page implementation), so the breadcrumb trail treated the previous screen as current and dropped the Videos crumb — or showed only the Stats root on direct entry. Register videodetails like postDetails (tracked, never rendered as a crumb) and record it in a layout effect so it lands in the history before the breadcrumb-trail hook's passive effect reads it. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR modernizes the VideoPress “video details” Stats view by moving it out of the generic stats summary switch into a dedicated async-loaded page, and rebuilding the UI to match the newer post-details patterns (header card, period navigation, and selectable metric cards). It also hardens the statsVideo normalizer to avoid throwing on an empty-window response shape from the API.
Changes:
- Route
/stats/:period/videodetails/:site?post=IDto a newstats-video-detailpage loaded directly from the stats controller (instead of rendering insidesummary/index.jsx). - Add a new video detail UI: video header/details card, chart with period navigation + metric selection, and an “Embedded pages” card.
- Make
normalizers.statsVideo()resilient to non-tuplepayload.dataresponses (empty-window shape).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| client/state/stats/lists/utils.js | Hardens statsVideo normalization to avoid crashes when payload.data isn’t an array of tuples. |
| client/my-sites/stats/summary/index.jsx | Removes the legacy videodetails branch and related data-fetching from the summary switch. |
| client/my-sites/stats/stats-video-summary/index.jsx | Deletes the legacy video summary chart component. |
| client/my-sites/stats/stats-video-details/index.jsx | Deletes the legacy “Video Embeds” module component. |
| client/my-sites/stats/stats-video-detail/index.tsx | Adds the new dedicated video details page (breadcrumb, tracking, media query, layout). |
| client/my-sites/stats/stats-video-detail/video-details-card.tsx | Adds the “Video details” header card (title/date/thumbnail + media library link). |
| client/my-sites/stats/stats-video-detail/video-summary.tsx | Implements the modernized chart, period navigation, and selectable metrics for video stats. |
| client/my-sites/stats/stats-video-detail/video-metric-tabs.tsx | Adds metric selection cards (Views/Impressions/Hours watched/Retention rate). |
| client/my-sites/stats/stats-video-detail/video-embeds-card.tsx | Adds the redesigned “Embedded pages” card for the video. |
| client/my-sites/stats/stats-video-detail/style.scss | Adds styling for the new video details page/cards/tabs layout. |
| client/my-sites/stats/stats-summary/index.jsx | Treats type="video" as “modernized” for summary chart rendering. |
| client/my-sites/stats/controller.jsx | Routes videodetails to the new async-loaded page module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const data = useSelector( | ||
| ( state ) => | ||
| getSiteStatsNormalizedData( state, siteId, 'statsVideo', query ) as VideoEmbedsData | null | ||
| ); | ||
| const isLoading = useSelector( ( state ) => | ||
| siteId ? isRequestingSiteStatsForQuery( state, siteId, 'statsVideo', query ) && ! data : ! data | ||
| ); | ||
|
|
||
| const pages = data?.pages ?? []; | ||
|
|
||
| return ( | ||
| <Card className="stats-video-embeds-card"> | ||
| { siteId && <QuerySiteStats siteId={ siteId } statType="statsVideo" query={ query } /> } | ||
| <h4 className="stats-video-embeds-card__heading">{ translate( 'Embedded pages' ) }</h4> | ||
| <StatsModulePlaceholder isLoading={ isLoading } /> | ||
| { ! isLoading && ! pages.length && ( | ||
| <div className="stats-video-embeds-card__empty"> | ||
| { translate( 'No pages have embedded this video yet.' ) } | ||
| </div> | ||
| ) } |
There was a problem hiding this comment.
Fixed in 2a6dfa6 — missing data is now treated as loading, so the placeholder shows until the deferred request resolves.
| // When the requested window has no rows at all, the endpoint returns a single | ||
| // `{ date, p }` object instead of the usual `[ date, value ]` tuples. | ||
| if ( Array.isArray( payload.data ) ) { | ||
| data = payload.data | ||
| .filter( ( item ) => Array.isArray( item ) ) |
There was a problem hiding this comment.
Added in 2a6dfa6 — two cases covering the bare object shape and a mixed array.
…eo-details-page # Conflicts: # client/my-sites/stats/controller.jsx
Include watch-time bucket keys in the chart bucket union, treat missing embeds data as loading so the empty state doesn't flash before the deferred request starts, and cover the statsVideo normalizer's empty-window response shapes with unit tests. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This Pull Request is now available for translation here: https://translate.wordpress.com/deliverables/34263347 Some locales (Brazilian Portuguese, Hebrew) have been temporarily machine-translated due to translator availability. All other translations are usually ready within a few days. Untranslated and machine-translated strings will be sent for translation next Monday and are expected to be completed by the following Friday. Hi @dognose24, could you please edit the description of this PR and add a screenshot for our translators? Ideally it'd include all of the following strings:
Thank you in advance! |
The Odyssey stats-app proxy (Jetpack stats-admin REST controller) has no media or settings routes, so those requests 404 in wp-admin. Gate the shared stats shell's QuerySiteSettings the same way as QuerySiteFeatures (the request 404s on every Odyssey stats page today), and hide the video details header card in Odyssey while its media item is unavailable instead of showing a loading state forever. The media request is still made, so the card and the derived retention rate light up automatically once the proxy learns the media route. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The VideoPress poster comes in arbitrary aspect ratios and sizes, which the card layout handled poorly; keep the card to the title and published date. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stats/video/:id response includes the video's attachment post, so the breadcrumb and header card can take the title and published date from data the page already fetches — and since the Odyssey stats-app proxy forwards stats routes, both now work in wp-admin too. The media item is only consulted as a fallback and for the video duration (retention rate), which remains adaptive. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin the period switcher to the right edge when the period header wraps on narrow widths, add breathing room between the header and the chart, and stretch the metric cards to the row height so they stay uniform when a label or value wraps inside one of them. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retention rate needs the video duration, which only exists on the media item — unavailable in Odyssey Stats where the stats-app proxy has no media route. Drop the card and its derived series for now; it can return once the backend provides a retention series (STATS-312) or the proxy learns a media route (Automattic/jetpack#50412). Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QuerySiteStats defers its initial request, so on the first render after switching to the Months/Years window the requesting flag is still false while the data is missing, and the chart briefly rendered its empty state — resizing the layout — before the fetch started. Treat a missing selected series as loading, matching the embeds card. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopts the identical normalizer hunk and tests from the video details modernization branch (Array.isArray guard on payload.data and the attachment post passthrough) so whichever PR lands second merges cleanly.
Nothing hides the chart while StatsModulePlaceholder shows inside .is-summary-chart (the .is-chart-tabs.is-loading rule targets the outer wrapper), so during loading the placeholder and the empty chart stacked to double height and the page jumped when data arrived — pin the section height and hide the chart while loading. Also lay the metric cards out with a fixed-column grid: the 50%-minus-gap flex basis could round past 100% and wrap cards one per row. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The single-column rule still targeted the old flex layout (flex-basis has no effect on grid items), so narrow viewports kept two columns. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reens The page container only adds side padding above the medium breakpoint, so the cards sat flush against the browser edge below it. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ellipsis rules on the current-crumb label never applied: each crumb wraps in an unclassed span that is the actual flex item, so nothing let the label shrink, and the label also lacked white-space: nowrap. Give the wrapper a class with min-width: 0 and complete the truncation rules — long video/post titles now ellipsize instead of overflowing. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nikschavan
left a comment
There was a problem hiding this comment.
Thank you, this tested well — added two non-blocking comments. Tested on Simple and Jetpack sites.
| data?: Array< { period: string; value: number } >; | ||
| } | ||
|
|
||
| const STAT_TYPES: VideoStatType[] = [ 'views', 'impressions', 'watch_time' ]; |
There was a problem hiding this comment.
Non-blocking: the All videos page still deep-links here with ?statType=retention_rate from its Retention Rate cells (videopress-stats-module/index.jsx), which now silently falls back to Views. Might be worth updating that link (or noting the fallback is intentional) since the retention card is gone.
There was a problem hiding this comment.
Intentional — the fallback also has to exist for old links, and the param regains meaning once retention returns (backend retention series in STATS-312, or the media proxy route in Automattic/jetpack#50412), so I left the All videos link untouched rather than churning it twice. The fallback is noted in the PR description.
|
|
||
| switch ( statType ) { | ||
| case 'watch_time': | ||
| return value > 1 |
There was a problem hiding this comment.
Non-blocking: value > 1 shows "< 1.0" for a value of exactly 1, and also when it's 0 — zero hours watched reading as "< 1.0" is a bit misleading. value >= 1 plus a zero case would cover both.
There was a problem hiding this comment.
Fixed in 7488356 — 0 and exact 1 now render as "0.0"/"1.0". (The > 1 quirk was inherited from videopress-stats-module's table, which still has it; left that for a separate cleanup since this PR doesn't otherwise touch it.)
The logo sits in JetpackTitle's HStack as a flex item with the default flex-shrink: 1, so it compressed alongside a truncating breadcrumb title; the previous pin targeted a logo inside the breadcrumbs nav, where none renders. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'value > 1' rendered both 0 and exactly 1 as '< 1.0' — zero hours reading as 'less than one' is misleading. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wp-admin core styles give h4 a 1.33em vertical margin in Odyssey Stats. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The card lost its shimmer when the title source moved to the statsVideo response and started rendering nothing until data arrived, pushing the chart down when it appeared. Reserve the card with shimmering title/date lines while neither source has responded, and keep hiding it only when the response confirms there is no title. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same as the details card heading: wp-admin core styles give h4 a 1.33em vertical margin in Odyssey Stats. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This Pull Request is now available for translation here: https://translate.wordpress.com/deliverables/34263347 Some locales (Brazilian Portuguese, Hebrew) have been temporarily machine-translated due to translator availability. All other translations are usually ready within a few days. Untranslated and machine-translated strings will be sent for translation next Monday and are expected to be completed by the following Friday. Hi @dognose24, could you please edit the description of this PR and add a screenshot for our translators? Ideally it'd include all of the following strings:
Thank you in advance! |
…atched formatting (Automattic#112528) * Stats: show exact zero and one hours watched on the All videos page 'watch_time > 1' rendered both 0 and exactly 1 as '< 1.0' — zero hours reading as 'less than one' is misleading. Matches the fix already applied to the video details metric cards in Automattic#112499. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: stop fetching the media item on the video details page It only remained as a title/date fallback after the retention card was removed, but the statsVideo response's attachment post is the primary source for both and is available in every environment — while the media request is a guaranteed 404 in Odyssey and redundant in Calypso. Part of STATS-299. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stats: end video details loading states when a stats request fails A failed statsVideo request left data null forever, so the header card shimmered indefinitely — and the chart and embeds card had the same gap. Derive loading from hasSiteStatsQueryFailed as well, so a failure resolves to the hidden card / empty states instead. Part of STATS-299 follow-ups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Part of STATS-299
Proposed Changes
/stats/:period/videodetails/:site?post=ID) out of thesummaryswitch into a dedicatedstats-video-detailpage modeled on the single post details page. The URL is unchanged; the controller now loads the new page directly.stats/videoresponse) and three selectable metric cards below the chart — Views, Impressions, Hours watched — showing totals for the window visible in the chart. Clicking a card switches the chart series. ThestatTypequery param still seeds the initial selection, so existing links from the All videos page keep working (statType=retention_ratefalls back to Views — see the note below).statsVideonormalizer against the endpoint's empty-window response (a bare{ date, p }object instead of[ date, value ]tuples), which would previously throw.stats-video-summaryandstats-video-detailscomponents, and the video-specific branches insummary/index.jsx.Notes on data:
stats/video/:videoIdendpoint per window. No new endpoints.playsmetric, same window).stats/video/:idalready returns (exposed via thestatsVideonormalizer), so the breadcrumb and header card work in both Calypso and Odyssey with no extra requests. The page makes no media-item request at all — the Odyssey stats-app proxy has no media route, and with retention gone nothing else needed it.complete_statsendpoint uses,(watch time / plays) / duration— needs the video duration, which only exists on the media item and is therefore unavailable in Odyssey. Removed for now so the page is identical everywhere; it can return once the backend provides a retention series (STATS-312) or the proxy learns a media route (Stats: add a single media item route to the stats-app proxy jetpack#50412, draft on hold). Sourcing retention fromvideo-plays?complete_stats=1instead was considered and rejected: it only returns per-video window totals (no series for the chart) and requires fetching the whole site's video list to read one row.QuerySiteSettingsis gated off in wp-admin, since the stats-app proxy has no settings route either (pre-existing 404 on every Odyssey stats page).pagesfield only returns URL strings. Showing post titles requires a backend change (tracked separately).stats/video/:videoIdendpoint limitations (pre-existing, confirmed by a backend investigation): the endpoint ignores date/range parameters and returns a fixed trailing window (period=day→ 2 days,week→ 7,month→ 30,year→ 365 with monthly buckets); buckets are daily except forperiod=year. It also only recognizesstatType=watch_time|impressions—views,plays, andretention_ratesilently fall back to the plays column, so there is no real retention series. Until the backend adds range parameters andstatType=all, the Views number here can cover a different window than the Traffic-page Videos module / All videos page (both metrics are the sameplayscolumn). Follow-up commits in this PR align the frontend to these actual semantics.Why are these changes being made?
Testing Instructions
/stats/day/videoplays/:site), then click a video (or one of its metric values) to open the video details page.?statType=impressions(etc.) param and confirm that metric is preselected.Pre-merge Checklist
🤖 Generated with Claude Code