Skip to content

feat(plugin): named page types as the unit for per-template config + metrics (v0.34.0) - #70

Open
harper-joseph wants to merge 1 commit into
mainfrom
feat/page-types
Open

feat(plugin): named page types as the unit for per-template config + metrics (v0.34.0)#70
harper-joseph wants to merge 1 commit into
mainfrom
feat/page-types

Conversation

@harper-joseph

Copy link
Copy Markdown
Contributor

The problem

A route says whether a path is prerendered. Nothing said what kind of page it is, so every template-shaped concern was keyed on the matched route's path:

  • route_serve / route_page_age (shipped v0.33.0) labelled by route.path. A site whose category pages are reachable by two URL shapes — e.g. prefix /catalog/ and contains /category/ — got two unrelated series that only a reader holding the route list could add back together.
  • render_time had no template dimension at all (its third recordAnalytics slot was unused). Render cost is the fleet capacity input, but it was one undifferentiated distribution — so "PDPs are expensive to settle" was a thing you could believe and not a thing you could show.
  • renderInterval attached to a pattern, so two routes sharing a template had to repeat it, and the copies could drift with only the first match ever observed.
  • The render fleet scopes waitFor rules by regex pathPattern in its own config — a second pattern language for the same routing fact, in another repo, with nothing keeping the two in step.

The change

A top-level pageTypes list of named templates that ingress.routes entries point at with pageType. Several routes may share one name; that is the point.

pageTypes:
  - { name: home, renderInterval: 7200000 }      # 2h
  - { name: category, renderInterval: 43200000 } # 12h
  - { name: pdp, renderInterval: 172800000 }     # 48h

ingress:
  routes:
    - { match: exact,    path: '/',              pageType: home }
    - { match: prefix,   path: '/catalog/',      pageType: category }
    - { match: contains, path: '/category/',     pageType: category }  # same template
    - { match: prefix,   path: '/product/prd-',  pageType: pdp }
area change
metrics route_serve/route_page_agepagetype_serve/pagetype_age, labelled by template
cost render_time gains the template in its free third dimension
cadence precedence becomes route > pageType > stored > default
job claim payload carries the declared name, for browser-side rule scoping
explain template, whether declared, and the resolved cadence
config a pageTypes entry no route references is an info finding

Design notes

pageType vs pageTypeLabel are deliberately separate. pageType is the declared name or null; pageTypeLabel is always a string (name → route path → route class). Metrics take the label because every request must produce one or the series develops holes that read as lost traffic. The queue job sends the declared name only — were it to send the fallback, a browser-side rule scoped to a template would fire on a route never declared to be one.

The label is computed inside the classification, not offered as a pageTypeLabel(x) helper. A helper would have to agree with each caller about what its argument is called (the read path says route, the classifier says entry) — exactly the drift routeClass.js exists to prevent.

render_time bills result.id, not cacheKey — the latter may already have been re-pointed at a redirect destination, which would bill the render to the template it landed on rather than the one that paid for it.

Config shape deviates from a keyed map. mergeInto walks the defaults shape, so an open-keyed pageTypes: {home: {...}} map would be silently dropped key by key. An array of named entries (same pattern as ingress.routes) gives identical declare-once/reference-by-name semantics with no surgery on the shared merge layer.

Compatibility

Adoption is incremental. With no pageType on any route the label falls back to the route path, then the route class — exactly the values emitted before. Name one route at a time; nothing resets.

The metric rename is a clean break, not a silent change of meaning. route_serve/route_page_age shipped days ago in v0.33.0 and the rollout dashboard reads only bot_request/bot_serve/page_age, which are untouched. Renaming avoids a metric whose name says "route" while its label is a template.

Label cardinality stays bounded by construction: every arm resolves to a configured name, a configured path, or one of three class constants. No arm derives a label from the request.

Testing

408 tests pass; lint and format clean. New coverage in routeClass.test.js (two routes → one label, the fallback chain, the four-level cadence precedence, malformed input dropping the field not the route, prefix-mode parity, duplicate names) and botServe.test.js (dimension order, verbatim label pass-through).

Follow-ups (this is PR 1 of 3)

  1. packages/browserWaitForRule.pageTypes, honoring job.pageType.
  2. render-service — swap pathPattern: '^/product/' for pageTypes: ['pdp'], collapsing the two pattern languages into one vocabulary owned here.

🤖 Generated with Claude Code

…metrics; v0.34.0

A route says WHETHER a path is prerendered. It could not say WHAT KIND of page it
is, so everything template-shaped was keyed on the matched route's path — and a
template reachable by two URL shapes became two unrelated things.

Adds a top-level `pageTypes` list of named templates (`home`, `category`, `pdp`)
that `ingress.routes` entries point at with `pageType`. Several routes may share
one name; that is the point.

  metrics   route_serve/route_page_age -> pagetype_serve/pagetype_age, labelled by
            template rather than route path. Two category routes now report as one
            series instead of two a reader had to know to add together.
  cost      render_time gains the template in its previously-unused third dimension.
            Render cost is the fleet capacity input but was recorded as one
            undifferentiated distribution, so "PDPs are expensive to settle" was
            believable and not showable. It now joins delivered freshness on one key.
  cadence   resolveRenderInterval precedence becomes route > pageType > stored >
            default. A cadence shared by several routes lives once, so the copies
            cannot drift with only the first match observed.
  job       the claim payload carries the DECLARED name (never the label's fallback),
            so browser-side render rules can scope by template instead of restating
            the same URL shapes as regexes in the render fleet's own config.
  explain   /prerender_admin reports the template, whether it is declared, and the
            cadence that follows from it.
  config    a pageTypes entry no route references is an `info` finding — that state
            is a spelling mismatch whose only other symptom is silence.

Adoption is incremental: with no `pageType` on any route the label falls back to the
route path and then the route class, which is exactly what shipped before. Label
cardinality stays bounded by construction — no arm derives a label from the request.

Renaming route_serve/route_page_age is a clean break rather than a silent change of
meaning; they shipped in v0.33.0 and no dashboard consumes them yet.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces "Page types (templates)" to group multiple routes under a single template name (such as home, category, or pdp) for unified metrics and configuration. It updates the configuration schema, ingress classification, render queue, admin UI, and analytics tracking to use page types instead of individual route paths. Feedback is provided regarding a performance optimization in packages/plugin/src/config.js where calling routePageTypes() inside a filter loop results in O(N * M) complexity, which can be optimized to O(N + M) by extracting the call outside the loop.

// its symptom is silence: the type's settings simply never apply and its name never appears
// in metrics, which reads exactly like "this template gets no traffic". Info, not warn — the
// same list is legitimately shared across deployments whose route sets differ.
const unusedPageTypes = declaredPageTypes().filter((name) => !routePageTypes().has(name));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling routePageTypes() inside the filter callback causes the route list to be iterated and a new Set to be constructed for every single declared page type. This results in an $O(N \times M)$ complexity where $N$ is the number of declared page types and $M$ is the number of routes. We can optimize this to $O(N + M)$ by calling routePageTypes() once outside the filter and storing the resulting Set in a local variable.

Suggested change
const unusedPageTypes = declaredPageTypes().filter((name) => !routePageTypes().has(name));
const referenced = routePageTypes(); const unusedPageTypes = declaredPageTypes().filter((name) => !referenced.has(name));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant