You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Set up the Key Metrics PDF rendering architecture and prove it end-to-end by rendering the first user-configured KM tile in the PDF. Unlike the section widgets (#12544-#12551, #12630) where each module's registerWidget call gains its own pdf field, Key Metrics composes per-user, per-tile content from the existing KEY_METRICS_WIDGETS map. This ticket extends that map with a new optional field, adds a reusable data-loader factory, and registers a single aggregate widget on the core widget registry that delegates to per-tile configs at export time.
Per the design doc's Key Metrics: Registry-Driven Rendering section:
KEY_METRICS_WIDGETS (in assets/js/components/KeyMetrics/key-metrics-widgets.js) gains an optional pdfTile: { TileComponent, getTileData } field per entry.
A new keyMetricsPDFSection widget is registered on CORE_WIDGETS in register-defaults.js. Its pdf.getData composes per-tile data from CORE_USER.getKeyMetrics() and each entry's pdfTile config; its pdf.Component (KeyMetricsPDF) renders the tiles in a 4-column grid.
A reusable createKeyMetricTileDataLoader factory builds each per-tile getTileData from a report-options builder and an extract function.
No new datastore selectors or actions. The aggregate getData and the pdf.isActive predicate compose existing selectors: CORE_USER.getKeyMetrics(), the KEY_METRICS_WIDGETS static map, the existing per-module fetchGetReport actions, and the pdf.isActive registry extension introduced by #12546.
MVP scope:
The schema extension and the aggregate widget are landed in full.
Tile shape variants beyond numeric (text-format, mini-table) are deferred; a later ticket can add PDFMetricTileText / PDFMetricTileTable primitives alongside PDFMetricTile.
Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
The KEY_METRICS_WIDGETS map accepts an optional pdfTile: { TileComponent, getTileData } field on each entry, documented in JSDoc alongside the existing fields.
A new keyMetricsPDFSection widget is registered on the core widget registry in the Key Metrics area with a pdf: { Component, getData, isActive } configuration that composes per-tile pdfTile configs at export time.
The orchestrator includes the Key Metrics section in the PDF only when the user's configured key metrics include at least one tile whose entry in KEY_METRICS_WIDGETS has a pdfTile field defined; otherwise the section does not appear in the sidesheet or the PDF.
When included, the Key Metrics section renders a 4-column grid of tiles, one tile per user-configured key metric that has a pdfTile config, in the user's configured order.
The KM_ANALYTICS_NEW_VISITORS entry receives a pdfTile config so a user with New Visitors in their selected Key Metrics sees that tile rendered in the PDF. KM entries without a pdfTile field do not render in the PDF and are skipped silently in the iteration.
The dashboard's Key Metrics rendering is unchanged: existing per-slug widget registrations continue to drive dashboard tile rendering, and the new aggregate keyMetricsPDFSection widget renders WidgetNull on the dashboard so no grid slot is occupied.
Feature gated behind the pdfGeneration feature flag.
Implementation Brief
Files to modify
Frontend - Key Metrics registry schema + first tile config
Update file assets/js/components/KeyMetrics/key-metrics-widgets.js - extend the JSDoc for the KEY_METRICS_WIDGETS export to document a new optional field per entry: pdfTile: { TileComponent: React.ComponentType<{ data }>, getTileData: ( { registry, dates, signal } ) => Promise<data> }. TileComponent is the @react-pdf/renderer component for the tile; getTileData returns the normalised data shape TileComponent consumes. Add the pdfTile field to the KM_ANALYTICS_NEW_VISITORS entry using PDFMetricTile (from Register the first PDF widget via the registry: Traffic → All Visitors (excluding chart generation) #12630) as the TileComponent and createKeyMetricTileDataLoader(...) (next bullet) as the getTileData, with the loader configured for the same newVsReturning GA4 report the dashboard's NewVisitorsWidget uses. No other entries are updated in this ticket.
Frontend - reusable tile data loader factory
Create file assets/js/components/KeyMetrics/create-key-metric-tile-data-loader.ts - exports createKeyMetricTileDataLoader( buildReports, extract ) which returns an async getTileData( { registry, dates, signal } ) function. buildReports( dates ) returns Array<{ moduleStore, options }> listing the reports to fetch; extract( reports ) receives the resolved report responses and returns the normalised data shape the TileComponent consumes (e.g. { title, currentValue, previousValue, change, sublabel } for a numeric tile). The returned getTileData:
Bails out early when signal.aborted between awaits.
Calls extract( reports ) and returns the result.
Frontend - aggregate widget
Create file assets/js/components/KeyMetrics/getPDFData.ts - default-exports getPDFData( { registry, dates, signal } ) for the aggregate widget. Resolves the user's KM slugs via registry.resolveSelect( CORE_USER ).getKeyMetrics(). Iterates the slugs, looks up KEY_METRICS_WIDGETS[ slug ]?.pdfTile, skips entries without a pdfTile field, and for each remaining slug calls pdfTile.getTileData( { registry, dates, signal } ). Returns { tiles: Array<{ slug, title, TileComponent, data }> } where title comes from KEY_METRICS_WIDGETS[ slug ].title and TileComponent comes from KEY_METRICS_WIDGETS[ slug ].pdfTile.TileComponent. Captures per-tile getTileData failures into a per-tile error map so the component can render a "Data unavailable" placeholder for that one tile while the others render normally. Throws only when every tile's getTileData fails. Bails out early when signal.aborted between awaits.
Create file assets/js/components/KeyMetrics/KeyMetricsPDF.tsx - @react-pdf/renderer aggregate component. Accepts { data: { tiles } } and renders a single <View> with flexDirection: 'row' and flexWrap: 'wrap', each tile at width: '50%' (with a small horizontal gutter per pdf-theme.ts). For each tile, renders <tile.TileComponent data={ tile.data } title={ tile.title } />. Renders a per-tile "Data unavailable" placeholder when an individual tile's data is null.
Frontend - register the aggregate widget
Update file assets/js/googlesitekit/widgets/register-defaults.js - register a new core widget keyMetricsPDFSection in AREA_MAIN_DASHBOARD_KEY_METRICS_PRIMARY. The dashboard Component returns <WidgetNull /> so no grid slot is occupied on the dashboard. The widget declares modules: [ MODULE_SLUG_ANALYTICS_4 ] so view-only filtering excludes the section when Analytics 4 is not shared, and a pdf configuration:
pdf.label is omitted: Key Metrics is a single-widget area per the design doc, so the sub-section heading is suppressed.
Test Coverage
Jest: assets/js/components/KeyMetrics/key-metrics-widgets.test.js - assert the KM_ANALYTICS_NEW_VISITORS entry has pdfTile.TileComponent and pdfTile.getTileData defined; assert every other entry's pdfTile field is undefined.
Jest: assets/js/components/KeyMetrics/create-key-metric-tile-data-loader.test.ts - the factory's returned function dispatches fetchGetReport for each buildReports( dates ) entry with signal threaded through; reports run in parallel via Promise.all; extract is called with the resolved responses in input order; returns the extract result; signal.aborted between dispatches short-circuits.
Jest: assets/js/components/KeyMetrics/getPDFData.test.ts - reads getKeyMetrics; iterates the slugs and skips those without pdfTile; calls each remaining tile's getTileData with { registry, dates, signal }; returns { tiles } in the user's configured order with title and TileComponent looked up from KEY_METRICS_WIDGETS; a single tile's getTileData failure captures into the per-tile error map without aborting the others; throws only when every tile fails; signal.aborted short-circuits.
Jest: assets/js/components/KeyMetrics/KeyMetricsPDF.test.tsx - renders one column per tile in a 4-column grid (flex row + wrap, 50% width); calls each tile's TileComponent with data={ tile.data } and title={ tile.title }; renders a per-tile "Data unavailable" placeholder when an individual tile's data is null; renders nothing (or the whole-widget placeholder) when tiles is empty.
Jest: assets/js/googlesitekit/widgets/register-defaults.test.js (extend) - assert keyMetricsPDFSection is registered in AREA_MAIN_DASHBOARD_KEY_METRICS_PRIMARY with a pdf field; the dashboard Component renders <WidgetNull />; pdf.isActive returns true when the user's getKeyMetrics selection includes KM_ANALYTICS_NEW_VISITORS and false when the selection contains only slugs without pdfTile.
No Storybook / VRT for the new *.tsx files: @react-pdf/renderer components render to a PDF document, not the DOM.
Feature Description
Set up the Key Metrics PDF rendering architecture and prove it end-to-end by rendering the first user-configured KM tile in the PDF. Unlike the section widgets (#12544-#12551, #12630) where each module's
registerWidgetcall gains its ownpdffield, Key Metrics composes per-user, per-tile content from the existingKEY_METRICS_WIDGETSmap. This ticket extends that map with a new optional field, adds a reusable data-loader factory, and registers a single aggregate widget on the core widget registry that delegates to per-tile configs at export time.Per the design doc's Key Metrics: Registry-Driven Rendering section:
KEY_METRICS_WIDGETS(inassets/js/components/KeyMetrics/key-metrics-widgets.js) gains an optionalpdfTile: { TileComponent, getTileData }field per entry.keyMetricsPDFSectionwidget is registered onCORE_WIDGETSinregister-defaults.js. Itspdf.getDatacomposes per-tile data fromCORE_USER.getKeyMetrics()and each entry'spdfTileconfig; itspdf.Component(KeyMetricsPDF) renders the tiles in a 4-column grid.createKeyMetricTileDataLoaderfactory builds each per-tilegetTileDatafrom a report-options builder and an extract function.No new datastore selectors or actions. The aggregate
getDataand thepdf.isActivepredicate compose existing selectors:CORE_USER.getKeyMetrics(), theKEY_METRICS_WIDGETSstatic map, the existing per-modulefetchGetReportactions, and thepdf.isActiveregistry extension introduced by #12546.MVP scope:
pdfTileconfig in this ticket:KM_ANALYTICS_NEW_VISITORS. It is the simplest numeric tile (one GA4 report, no charts) and reusesPDFMetricTilefrom Register the first PDF widget via the registry: Traffic → All Visitors (excluding chart generation) #12630 directly as itsTileComponent. Subsequent tickets addpdfTileconfigs to the other KM entries as one-line additions.PDFMetricTileText/PDFMetricTileTableprimitives alongsidePDFMetricTile.Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
KEY_METRICS_WIDGETSmap accepts an optionalpdfTile: { TileComponent, getTileData }field on each entry, documented in JSDoc alongside the existing fields.keyMetricsPDFSectionwidget is registered on the core widget registry in the Key Metrics area with apdf: { Component, getData, isActive }configuration that composes per-tilepdfTileconfigs at export time.KEY_METRICS_WIDGETShas apdfTilefield defined; otherwise the section does not appear in the sidesheet or the PDF.pdfTileconfig, in the user's configured order.KM_ANALYTICS_NEW_VISITORSentry receives apdfTileconfig so a user with New Visitors in their selected Key Metrics sees that tile rendered in the PDF. KM entries without apdfTilefield do not render in the PDF and are skipped silently in the iteration.keyMetricsPDFSectionwidget rendersWidgetNullon the dashboard so no grid slot is occupied.pdfGenerationfeature flag.Implementation Brief
Files to modify
Frontend - Key Metrics registry schema + first tile config
assets/js/components/KeyMetrics/key-metrics-widgets.js- extend the JSDoc for theKEY_METRICS_WIDGETSexport to document a new optional field per entry:pdfTile: { TileComponent: React.ComponentType<{ data }>, getTileData: ( { registry, dates, signal } ) => Promise<data> }.TileComponentis the@react-pdf/renderercomponent for the tile;getTileDatareturns the normalised data shapeTileComponentconsumes. Add thepdfTilefield to theKM_ANALYTICS_NEW_VISITORSentry usingPDFMetricTile(from Register the first PDF widget via the registry: Traffic → All Visitors (excluding chart generation) #12630) as theTileComponentandcreateKeyMetricTileDataLoader(...)(next bullet) as thegetTileData, with the loader configured for the samenewVsReturningGA4 report the dashboard'sNewVisitorsWidgetuses. No other entries are updated in this ticket.Frontend - reusable tile data loader factory
assets/js/components/KeyMetrics/create-key-metric-tile-data-loader.ts- exportscreateKeyMetricTileDataLoader( buildReports, extract )which returns an asyncgetTileData( { registry, dates, signal } )function.buildReports( dates )returnsArray<{ moduleStore, options }>listing the reports to fetch;extract( reports )receives the resolved report responses and returns the normaliseddatashape theTileComponentconsumes (e.g.{ title, currentValue, previousValue, change, sublabel }for a numeric tile). The returnedgetTileData:buildReports( dates )and dispatchesfetchGetReport( options, { signal } )on eachmoduleStorein parallel viaPromise.all(uses the per-store signal opt-in from Cancel underlying fetch requests when PDF generation is cancelled or fails #12699).signal.abortedbetween awaits.extract( reports )and returns the result.Frontend - aggregate widget
assets/js/components/KeyMetrics/getPDFData.ts- default-exportsgetPDFData( { registry, dates, signal } )for the aggregate widget. Resolves the user's KM slugs viaregistry.resolveSelect( CORE_USER ).getKeyMetrics(). Iterates the slugs, looks upKEY_METRICS_WIDGETS[ slug ]?.pdfTile, skips entries without apdfTilefield, and for each remaining slug callspdfTile.getTileData( { registry, dates, signal } ). Returns{ tiles: Array<{ slug, title, TileComponent, data }> }wheretitlecomes fromKEY_METRICS_WIDGETS[ slug ].titleandTileComponentcomes fromKEY_METRICS_WIDGETS[ slug ].pdfTile.TileComponent. Captures per-tilegetTileDatafailures into a per-tile error map so the component can render a "Data unavailable" placeholder for that one tile while the others render normally. Throws only when every tile'sgetTileDatafails. Bails out early whensignal.abortedbetween awaits.assets/js/components/KeyMetrics/KeyMetricsPDF.tsx-@react-pdf/rendereraggregate component. Accepts{ data: { tiles } }and renders a single<View>withflexDirection: 'row'andflexWrap: 'wrap', each tile atwidth: '50%'(with a small horizontal gutter perpdf-theme.ts). For each tile, renders<tile.TileComponent data={ tile.data } title={ tile.title } />. Renders a per-tile "Data unavailable" placeholder when an individual tile'sdatais null.Frontend - register the aggregate widget
assets/js/googlesitekit/widgets/register-defaults.js- register a new core widgetkeyMetricsPDFSectioninAREA_MAIN_DASHBOARD_KEY_METRICS_PRIMARY. The dashboardComponentreturns<WidgetNull />so no grid slot is occupied on the dashboard. The widget declaresmodules: [ MODULE_SLUG_ANALYTICS_4 ]so view-only filtering excludes the section when Analytics 4 is not shared, and apdfconfiguration:pdf.Component:KeyMetricsPDF.pdf.getData: the aggregategetPDFDatafrom above.pdf.isActive: ( select ) => ( select( CORE_USER ).getKeyMetrics() || [] ).some( ( slug ) => !! KEY_METRICS_WIDGETS[ slug ]?.pdfTile ). Uses the optionalpdf.isActivepredicate introduced by Implement PDF Widgets for "Traffic" -> "Your visitor groups" #12546.pdf.labelis omitted: Key Metrics is a single-widget area per the design doc, so the sub-section heading is suppressed.Test Coverage
assets/js/components/KeyMetrics/key-metrics-widgets.test.js- assert theKM_ANALYTICS_NEW_VISITORSentry haspdfTile.TileComponentandpdfTile.getTileDatadefined; assert every other entry'spdfTilefield isundefined.assets/js/components/KeyMetrics/create-key-metric-tile-data-loader.test.ts- the factory's returned function dispatchesfetchGetReportfor eachbuildReports( dates )entry withsignalthreaded through; reports run in parallel viaPromise.all;extractis called with the resolved responses in input order; returns theextractresult;signal.abortedbetween dispatches short-circuits.assets/js/components/KeyMetrics/getPDFData.test.ts- readsgetKeyMetrics; iterates the slugs and skips those withoutpdfTile; calls each remaining tile'sgetTileDatawith{ registry, dates, signal }; returns{ tiles }in the user's configured order withtitleandTileComponentlooked up fromKEY_METRICS_WIDGETS; a single tile'sgetTileDatafailure captures into the per-tile error map without aborting the others; throws only when every tile fails;signal.abortedshort-circuits.assets/js/components/KeyMetrics/KeyMetricsPDF.test.tsx- renders one column per tile in a 4-column grid (flex row + wrap, 50% width); calls each tile'sTileComponentwithdata={ tile.data }andtitle={ tile.title }; renders a per-tile "Data unavailable" placeholder when an individual tile'sdatais null; renders nothing (or the whole-widget placeholder) whentilesis empty.assets/js/googlesitekit/widgets/register-defaults.test.js(extend) - assertkeyMetricsPDFSectionis registered inAREA_MAIN_DASHBOARD_KEY_METRICS_PRIMARYwith apdffield; the dashboardComponentrenders<WidgetNull />;pdf.isActivereturnstruewhen the user'sgetKeyMetricsselection includesKM_ANALYTICS_NEW_VISITORSandfalsewhen the selection contains only slugs withoutpdfTile.No Storybook / VRT for the new
*.tsxfiles:@react-pdf/renderercomponents render to a PDF document, not the DOM.QA Brief
Changelog entry