Repository navigation
console v0.8.0: read the change probe and the discovery gate - #129
Conversation
The console last caught up at plugin v0.51.0. Since then the plugin shipped three capabilities the console could not see, and its own cross-package guard tests said so: `change-probe` and `discovery-purge` were dispatched upstream and reachable from nowhere, and `discovery_gated` was emitted and charted by no panel. REACHABILITY. Both routes join PROXIED_GET/PROXIED_POST, and both get a MERGER rather than the shared single-node answer. Neither one sums the way analytics does — a probe pass and a purge pass each cover the keys ONE node owns — so the merge produces every node's own slice side by side plus the running tally, and names the nodes that have not run. The alternative is the failure this classification exists to prevent: one node's "deleted 240,118" answered under a cluster label, which reads as done when three quarters of the keyspace has not been touched. Both POSTs are NODE_LOCAL_POST; the probe refuses for a second reason on top of residency, since its rate cap is a promise made to whoever runs the origin, per node. CHANGE PROBE (new view). The one freshness surface that does not measure pages against a cadence, so it gets its own page rather than a card on one. Two numbers are only meaningful together and sit together: the change rate, measured against the probes that HAD a baseline — a pass that is mostly seeding has compared nothing, and dividing by `probed` would report that as a stable catalogue — and the failure share, because a probe whose endpoint changed shape reports zero changes, triggers nothing, and is indistinguishable from content that is not moving. The canary's verdict is reported per node: a trip is a threshold crossed against one node's own cohort, and folding four of them into a boolean would name no node and match no log line. A trip that recorded no invalidation says which of the three reasons it was. `changeProbe` is edited here, and Config's index points at it. DISCOVERY GATE, split across the views that own its halves. Traffic gets the signal — `discovery_gated` by gate and by bot (it carries a bot, so the filter applies), plus a `discovery` column on the route table, since a gated route's misses never become targets. Overview gets the purge, beside the key-rule orphan sweep it is a sibling of: census first, then delete, with the gate-first interlock stated on the card rather than left to the plugin's 400 — with the route still discovering, crawlers re-mint exactly what a purge removes. A node that has never purged says so instead of rendering as a clean census of zeroes. Also: `ingress.routes[].demandFloor` rides the cadence join and prints beside the interval it bounds. 45 new tests. The five that matter were each proved by reverting the behaviour and watching them fail: the compared-not-probed denominator, value-summed pass counters, the bot filter on the gate panel, oldest-not-newest pass stamping, and the census precondition on the purge button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the 'Change probe' view and 'Discovery gate' functionality to the prerender console, enabling operators to monitor and manage content freshness based on actual origin changes rather than fixed cadences. It includes new UI views, route aggregation logic, and comprehensive test suites. The review feedback suggests importing and utilizing the ratioOf helper in the new probe.js view to safely calculate the probe failure rate and avoid potential division-by-null issues.
| import { | ||
| emptyNote, | ||
| fmtCount, | ||
| isMerged, | ||
| legend, | ||
| pick, | ||
| rangePicker, | ||
| scanFooter, | ||
| scopeLabel, | ||
| segmented, | ||
| stackBy, | ||
| stackedBars, | ||
| sumValues, | ||
| windowEmpty, | ||
| } from '../charts.js'; |
There was a problem hiding this comment.
Import ratioOf from ../charts.js to safely calculate ratios and avoid the Number(null) === 0 trap.
| import { | |
| emptyNote, | |
| fmtCount, | |
| isMerged, | |
| legend, | |
| pick, | |
| rangePicker, | |
| scanFooter, | |
| scopeLabel, | |
| segmented, | |
| stackBy, | |
| stackedBars, | |
| sumValues, | |
| windowEmpty, | |
| } from '../charts.js'; | |
| import { | |
| emptyNote, | |
| fmtCount, | |
| isMerged, | |
| legend, | |
| pick, | |
| rangePicker, | |
| ratioOf, | |
| scanFooter, | |
| scopeLabel, | |
| segmented, | |
| stackBy, | |
| stackedBars, | |
| sumValues, | |
| windowEmpty, | |
| } from '../charts.js'; |
References
- When calculating ratios or dividing metrics by a yardstick/cadence in JavaScript, avoid the
Number(null) === 0trap (wherenull / numberevaluates to0instead ofnullorNaN). Use a helper function to explicitly validate that both the value and the yardstick are finite numbers and that the yardstick is greater than 0 before performing the division, returningnullotherwise.
There was a problem hiding this comment.
Done in 9deb283 — ratioOf is imported and used at the threshold. See the reply on the other thread for why it was adopted as a convention rather than as a null-safety fix (sumValues cannot return null), and for the real defect that reading this site turned up.
| // Compared = probes that had a baseline to compare against. Seeds and failures had none, so | ||
| // including them in the denominator understates the drift rate by exactly the seeding backlog. | ||
| const compared = Math.max(0, probed - seeded - failed); | ||
| const failing = probed > 0 && failed / probed > FAILURE_ALARM; |
There was a problem hiding this comment.
Avoid the Number(null) === 0 trap when dividing metrics by using the ratioOf helper function. This ensures that if either failed or probed is null or undefined, the result is handled safely rather than incorrectly evaluating to 0.
| const failing = probed > 0 && failed / probed > FAILURE_ALARM; | |
| const failing = ratioOf(failed, probed) > FAILURE_ALARM; |
References
- When calculating ratios or dividing metrics by a yardstick/cadence in JavaScript, avoid the
Number(null) === 0trap (wherenull / numberevaluates to0instead ofnullorNaN). Use a helper function to explicitly validate that both the value and the yardstick are finite numbers and that the yardstick is greater than 0 before performing the division, returningnullotherwise.
There was a problem hiding this comment.
Adopted in 9deb283 — but not for the stated reason, and the difference is worth recording.
failed and probed here come from sumValues, which reduces from 0 over finite products
(Number.isFinite(s.mean) && s.count > 0). Both are therefore always numbers, never null, so the
Number(null) === 0 trap cannot arise at this site — and probed > 0 already covered the
zero-denominator case that ratioOf guards.
It still goes through the helper, because the guard belongs at the call site by convention rather
than by an argument a reader has to reconstruct from sumValues's contract. The ratio is now named
and compared explicitly, so nothing rests on null > 0.5 either:
const failureShare = ratioOf(failed, probed);
const failing = failureShare !== null && failureShare > FAILURE_ALARM;Re-reading that site did turn up a real defect next door, and it was mine: mergeChangeProbe takes
the FIRST error it finds across nodes, so an error and a full set of counters arrive together
whenever three nodes swept and one threw. The sweep card treated the error as the whole answer and
dropped the three good slices. Both are shown now, with the error saying the counters cover only the
passes that finished — and an errored pass that counted nothing still shows the error alone rather
than a row of dashes. Two tests added for it, plus one pinning that an empty window (ratio null)
never reads as "probe failures dominate".
…sweep failure that kept its numbers Review (gemini-code-assist) on #129: use `ratioOf` for the probe failure share. Adopted, though not for the stated reason — `sumValues` reduces from 0 over finite products, so neither side can be null and the `Number(null) === 0` trap cannot arise at this site. It goes through the helper because that is where the guard belongs by convention rather than by an argument a reader has to reconstruct from `sumValues`'s contract, and the ratio is now named and compared explicitly so nothing rests on `null > 0.5` either. Reading that site turned up a real one next door, and it is mine: the cluster merge takes the FIRST error it finds across nodes, so an error and a full set of counters arrive together whenever three nodes swept and one threw. The sweep card treated the error as the whole answer and dropped the three good slices. Both are now shown, and the error says the counters cover only the passes that finished — while an errored pass that counted nothing still shows the error alone rather than a row of dashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Console catch-up to plugin v0.55.0. The console last caught up at plugin v0.51.0; since then
the plugin shipped three capabilities the console could not see — and its own cross-package guard
tests said so on a fresh checkout:
change-probeanddiscovery-purgewere dispatched upstream and reachable from nowhere, anddiscovery_gatedwas emitted and read by no panel. This wires all of it up.Console 0.7.0 → 0.8.0. No plugin change, no schema change, no new endpoint — a drop-in over
0.7.0 on any cluster running plugin 0.53.0+ (older plugins simply have no probe routes; every new
panel degrades to its "not configured" state).
Reachability, and how the two routes aggregate
Both join
PROXIED_GET/PROXIED_POST, and both get a merger rather than the single-nodeshared answer.
Neither one sums the way analytics does — a probe pass and a purge pass each cover the keys one
node owns — so the merge produces every node's own slice side by side plus the running tally, and
names the nodes that have not run. The alternative is precisely the failure this classification
exists to prevent: one node's "deleted 240,118" answered under a cluster label, which reads as done
when three quarters of the keyspace has not been touched.
Both POSTs are
NODE_LOCAL_POST. The probe refuses under cluster scope for a second reason on topof residency: its rate cap is a promise made to whoever runs the origin, per node, and fanning
the pass out would spend it N times over.
Change probe — a new view
The one freshness surface in this console that does not measure pages against a cadence, so it gets
its own page rather than a card on someone else's.
Two numbers are only meaningful together, and now sit together:
seeding has compared nothing; dividing by
probedwould report that as a stable catalogue. Inthe test fixture the honest figure is 12% and the naive one is 6% — same catalogue, different
verdict.
nothing, and is indistinguishable from content that is not moving. Past 50% it becomes a banner
saying those pages are back on interval-only freshness.
The canary's verdict is reported per node — a trip is a threshold crossed against one node's own
cohort, and folding four of them into a boolean would name no node and match no log line. The
pooled fraction rides alongside as the wider sample it is. A trip that recorded no invalidation says
which of the three reasons it was (dry run, holdoff, unresolvable scope), and an unresolvable scope
reads as the fault it is.
Manual passes run from here (
sweep/canary), with an explicit run-mode picker:dryRunis thedifference between measuring and acting, and a POST that omits it inherits the configured value, so
a button labelled only "Run sweep" means different things on two deployments.
changeProbeisedited on this view; Config's index points at it.
Discovery gate — split across the views that own its halves
Traffic gets the signal:
discovery_gatedsplit by which gate refused and ranked by bot (itcarries a bot, so the existing client-side filter applies), plus a
discoverycolumn on the routetable — a gated route's misses never become targets, which is worth seeing next to its miss rate. A
deployment that has configured no gate gets the capability described rather than an empty chart.
Overview gets the purge, beside the key-rule orphan sweep it is a sibling of: census first, then
delete, stop, live progress. The gate-first interlock is stated on the card rather than left to
the plugin's 400 — with the route still discovering, crawlers re-mint exactly what a purge removes.
A node that has never purged says so instead of rendering as a clean census of zeroes.
Also:
ingress.routes[].demandFloor(v0.55.0) rides the same cadence join and prints beside theinterval it bounds.
Tests
182 pass, 0 fail (45 new). Repo lint and
format:checkclean; the plugin's 823 still pass.New:
test/probeView.test.js(16) andtest/overviewView.test.js(11) execute their views againstrealistic payloads, plus 13 merger tests in
aggregate.test.jsand 5 discovery-gate tests intrafficView.test.js.The five assertions that matter were each proved by reverting the behaviour and watching them fail:
compared→probeddenominatorsumValues→ emit countskeepBoton the gate pickminOf→maxOfon pass timeNotes for the reviewer
packages/plugin/README.mddocumentschange-probebut notdiscovery-purge,discoverTargets,discoveryBotsordemandFloor— v0.54.0/v0.55.0 landed with the prose onlyin
configSchema.js. Left alone here to keep this PR console-only and off the plugin releasetrain; worth a follow-up.
src/util/aggregate.jscontains three deliberate NUL bytes (comboKey's field separator), whichmakes
filecall it "data" and makes BSDgrepskip it silently unless you pass-a. Notchanged here, but it is a trap for anyone grepping the console.
🤖 Generated with Claude Code