Skip to content

Implement first Key Metric PDF widget through the registry to PDF #12554

Description

@benbowler

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

Image

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.
  • Exactly one KM tile gets a pdfTile config in this ticket: KM_ANALYTICS_NEW_VISITORS. It is the simplest numeric tile (one GA4 report, no charts) and reuses PDFMetricTile from Register the first PDF widget via the registry: Traffic → All Visitors (excluding chart generation) #12630 directly as its TileComponent. Subsequent tickets add pdfTile configs to the other KM entries as one-line additions.
  • 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:

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.Component: KeyMetricsPDF.
    • pdf.getData: the aggregate getPDFData from above.
    • pdf.isActive: ( select ) => ( select( CORE_USER ).getKeyMetrics() || [] ).some( ( slug ) => !! KEY_METRICS_WIDGETS[ slug ]?.pdfTile ). Uses the optional pdf.isActive predicate introduced by Implement PDF Widgets for "Traffic" -> "Your visitor groups" #12546.
    • 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.

QA Brief

Changelog entry

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Medium priorityTeam SIssues for Squad 1Type: EnhancementImprovement of an existing feature

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions