Skip to content

feat(plugin): split swr out of cache-hit + per-route serve/age metrics; v0.33.0 - #69

Open
harper-joseph wants to merge 2 commits into
mainfrom
feat/swr-split-telemetry
Open

feat(plugin): split swr out of cache-hit + per-route serve/age metrics; v0.33.0#69
harper-joseph wants to merge 2 commits into
mainfrom
feat/swr-split-telemetry

Conversation

@harper-joseph

Copy link
Copy Markdown
Contributor

Why

Phase 1 of TTL-decision telemetry: make "should this route's renderInterval move up or down" answerable from recorded metrics instead of a one-off investigation. Two gaps found while tuning the kohls TTLs:

  1. An SWR serve is recorded as hit. cacheStatus: 'hit' covers everything inside expiresAt + swrTtl, so the headline hit rate can't distinguish "TTL is being met" from "we're serving out of the stale window because the re-render is late". At one measured point, 71.9% "hit" quietly included ~13% of the corpus being served past expiry.
  2. Nothing is recorded per route. TTLs are per route (home 1h / catalog 6h / PDP 48h on kohls), but bot_serve/page_age aggregate across all of them, so a per-route TTL decision has no data.

What

  • bot_serve cacheStatus splits hithit | swr. hit = expiresAt still ahead (the renderInterval is being met); swr = past expiresAt, inside the stale-while-revalidate window. The serve itself is unchanged, and the source dimension is untouched (origin-offload reads identically). The x-harper-cache debug header reports the same split.
  • New route_serve (route, cacheStatus, deviceType) — per-route delivery: swr/stale share says whether that route's cadence is being delivered; miss share says whether its corpus is covered.
  • New route_page_age (route, cacheStatus, deviceType; ms since render, cache-served only) — served age against that route's own interval: the direct "should this TTL move" number.
  • The hit/swr boundary is one exported pure function, cacheServeStatus(expiresAtMs, swrTtl, now), with its edges pinned in tests (expiry instant is already swr; window edge exclusive; NaN never serves; swrTtl: 0 disables the window).

Why separate metrics instead of a 4th dimension

recordAnalytics has exactly three dimension slots (path/method/type — verified in harper-pro dist/core/resources/analytics/write.js), and bot_serve uses all three. Route label cardinality is tiny and stable: the matched route's path, else the route class, else 'unrouted'.

Why the request path, not the backlog snapshot

The kohls deployment just disabled the 15-min backlog snapshot timer (kohls-pr#52) because its getRecordCount walk issues a multi-second synchronous native iteration that stalls worker 0 and bot serving (harper-pro#664). These metrics ride the request path instead: two boolean counter bumps per request, two numeric samples on cache serves, buffered in-memory by recordAnalytics — no storage touch, no await, nothing on the response path, and nothing that runs when no bots are being served.

Breaking-ish

Dashboards keying bot_serve on cacheStatus='hit' will see it split into hit + swr after this release — sum them to recover the old cache-served rate. Dimension order is unchanged everywhere.

Tests

392/392 pass (7 in botServe.test.js, covering dimension order for all four metrics, the route-label fallback chain, swr propagation, the age guards, and the cacheServeStatus edges). Lint + format clean.

Follow-ups (not this PR)

  • Per-route swrTtl alongside renderInterval (the global value can't fit 1h/6h/48h intervals at once).
  • Admin Overview panel reading these metrics (needs the node-local vs cluster aggregation decision noted in admin/views/overview.js).
  • Phase 2: content-drift sampling (cached snapshot vs live origin per route) — separate project, hosting decision pending.

…s; v0.33.0

TTL-tuning telemetry, so raising or lowering a route's renderInterval can be decided
from a dashboard instead of a one-off investigation. Request-path counters only
(recordAnalytics in-memory buffers) — deliberately NOT the backlog snapshot, whose
timer is disabled on the kohls deployment because its native count walk stalls
worker 0 (harper-pro#664).

- bot_serve cacheStatus now distinguishes 'hit' (expiresAt still ahead — the page's
  own renderInterval is being met) from 'swr' (past expiresAt, served from the
  stale-while-revalidate window while the re-render is late/in flight). Both are
  cache serves; the serve itself is unchanged, as is the source dimension (origin
  offload reads the same). Folding both into 'hit' made the headline hit rate
  unreadable as a freshness signal: at one measured point 71.9% "hit" quietly
  included ~13% of the corpus being served past expiry. The x-harper-cache debug
  header reports the same split.

- New route_serve (route, cacheStatus, deviceType) and route_page_age (same dims,
  ms since render, cache-served only). recordAnalytics has exactly three dimension
  slots (path/method/type) and bot_serve's are all taken, hence separate metrics
  rather than a fourth dimension. Route label = matched route path ('/',
  '/catalog/', '/product/prd-'), else route class, else 'unrouted' — tiny, stable
  cardinality. Per route these answer: is the cadence delivered (swr/stale share),
  is the corpus covered (miss share), and what age are bots actually served
  (route_page_age vs that route's own renderInterval).

- The hit/swr boundary is extracted as cacheServeStatus(expiresAtMs, swrTtl, now),
  the single source of truth for cache servability, and its edges are pinned in
  tests: the expiry instant itself is already swr, the swr edge is exclusive,
  NaN never serves, swrTtl 0 disables the window.

Cost per request: two boolean counter bumps, plus two numeric samples on cache
serves. No storage touch, no await, nothing on the response path.

Dashboards keying bot_serve on cacheStatus='hit' will see hit split into
hit + swr after this release — sum them for the old cache-served rate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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 bumps the package version to 0.33.0 and introduces per-route analytics tracking for the prerender plugin. Specifically, it adds route_serve and route_page_age metrics to allow independent tuning of each route's render interval. It also refactors the cache freshness logic by introducing a cacheServeStatus helper function to distinguish between 'hit' and 'swr' (stale-while-revalidate) cache statuses. Comprehensive unit tests have been added to verify these new metrics and helper logic. There are no review comments to address, and I have no additional feedback to provide.

…he admin's three freshness copies onto it

Self-review follow-up. The hit/swr boundary was defined in bot_request.js while
PrerenderAdmin.js still carried three hand-copied `!isNaN(expiresAtMs) &&
expiresAtMs + swrTtl > now` computations — one of them commented "same freshness
rule the serving path applies, so this cannot disagree", which was aspirational,
not structural. Now it is structural: one exported function in util/pageFreshness.js
(a util home, so resources don't import from a handler module), and every consumer
— serve path, explain, pages view — calls it. When per-route swrTtl lands, it lands
everywhere at once.

renderNow's `lastCached >= since` check deliberately stays separate: that is "was
this re-rendered after my request started", not swr servability.

No behavior change; 392/392 tests (cacheServeStatus edges now pinned against the
util module).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@harper-joseph

Copy link
Copy Markdown
Contributor Author

Self-review pass for efficiency + maintainability, one commit added:

Efficiency (verified, no change needed): the serve path gained two numeric comparisons (cacheServeStatus — NaN falls out of the comparisons, so the old isNaN call is actually gone) and two recordAnalytics calls per request — each a string-key Map bump, no allocation, no await, gated on analytics.enabled. Nothing rides the backlog snapshot (disabled on kohls per harper-pro#664).

Maintainability (fixed): PrerenderAdmin.js carried three hand-copied freshness computations, one commented "same rule the serving path applies, so this cannot disagree" — aspirational, not structural. cacheServeStatus moved to util/pageFreshness.js (resources shouldn't import from a handler module) and all three admin sites now call it. grep confirms zero remaining copies. renderNow's lastCached >= since deliberately stays separate — different concept (re-rendered since request start, not swr servability).

392/392 tests, lint + format clean.

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