diff --git a/package-lock.json b/package-lock.json index 7317850f..823328b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8556,7 +8556,7 @@ }, "packages/console": { "name": "@harperfast/prerender-console", - "version": "0.13.0", + "version": "0.13.1", "license": "Apache-2.0", "dependencies": { "undici": "^7.18.2" diff --git a/packages/console/package.json b/packages/console/package.json index 5d2c05b3..2c5dcce1 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@harperfast/prerender-console", - "version": "0.13.0", + "version": "0.13.1", "type": "module", "description": "Standalone Harper component serving the prerender management console UI, proxying to a prerender deployment's /prerender_admin API", "license": "Apache-2.0", diff --git a/packages/console/src/admin/views/queue.js b/packages/console/src/admin/views/queue.js index bc49ca6a..1a930704 100644 --- a/packages/console/src/admin/views/queue.js +++ b/packages/console/src/admin/views/queue.js @@ -311,12 +311,54 @@ function supply(ctx) { spacer(), scanFooter(data), ]), + legacyRenderers(data), kpis, el('div', { cls: 'cols' }, [outcomeChart, timesChart]), reasons, ]); } +/** + * A renderer too old for URL jobs — `prerender_ops.legacy_renderer`, one per result posted in the + * single-device shape for a job that asked for several (a fleet pod predating browser 1.23.0). + * + * SHOWN ONLY WHEN NON-ZERO, and deliberately not a tile or a chart. Zero is the entire steady + * state, so a permanent "Legacy renderers: 0" would be noise on every healthy fleet and the + * silence would stop meaning anything. Non-zero is a mid-upgrade fleet, which is rare, temporary, + * and worth interrupting for. + * + * WHY IT IS WORTH INTERRUPTING FOR. Nothing else on this page moves. The old pod claims a URL job, + * renders ONE of its devices, and posts a well-formed result: the render counts as a success, the + * outcome chart is clean, render time is normal, and the devices it did not render simply go + * unrendered. Their cached pages then expire on their own schedule and those bots fall through to + * the origin — so the first visible symptom is a serve-side one, on a different view, days later. + * The plugin also warns hourly per node rather than per result, so the log is deliberately quiet. + */ +function legacyRenderers(data) { + const rows = pick(data, 'prerender_ops', (s) => s.path === 'legacy_renderer'); + const total = sumCount(rows); + if (!total) return null; + + // The slot carries the device the old renderer DID render, which is the useful half: the + // unrendered devices are every other one in `deviceTypes`, and naming what did arrive is what + // lets an operator recognise the pod's configuration. + const devices = [...new Set(rows.map((s) => s.method).filter(Boolean))].sort(); + + return card('Renderers that predate URL jobs', { + head: [spacer(), pill(`${num(total)} result${total === 1 ? '' : 's'}`, 'bad')], + body: [ + el('p', { cls: 'note bad' }, [ + `${num(total)} result${total === 1 ? ' was' : 's were'} posted in the single-device shape for a job `, + 'that asked for several, so some pod in the render fleet is older than browser 1.23.0. It rendered ', + devices.length ? el('code', { text: devices.join(', ') }) : 'one device', + ' and left the rest of each job unrendered — those pages are not failing, they are simply never ', + 'being written, and they will fall out of cache as they expire. Upgrade the fleet; this returns to ', + 'zero within one window of the last old pod being replaced.', + ]), + ], + }); +} + function cluster(ctx, data, setPause) { const control = data.control.cluster; diff --git a/packages/console/test/queueView.test.js b/packages/console/test/queueView.test.js index 954478d4..353cdeb7 100644 --- a/packages/console/test/queueView.test.js +++ b/packages/console/test/queueView.test.js @@ -218,3 +218,47 @@ test('an older plugin that emits none of this renders no panel at all', async () await load(ctx); assert.doesNotMatch(draw(ctx).textContent, /Render prioritisation/); }); + +/** + * `prerender_ops.legacy_renderer` — a fleet pod older than browser 1.23.0 posting a single-device + * result for a URL job. It shipped with plugin v0.66.0 and no console view read it, which the + * metric-coverage guard in adminAssets.test.js caught; these pin the reading rather than its + * mere presence. + * + * The silence is the point. An old pod's result is well-formed, so the outcome chart stays clean, + * render time stays normal, and the devices it never rendered just go unwritten — the first + * visible symptom is a serve-side one, days later, on a different view. + */ + +test('a healthy fleet draws no legacy-renderer panel at all — zero is the steady state', async () => { + const ctx = await ready(); + assert.doesNotMatch(draw(ctx).textContent, /predate URL jobs/); +}); + +test('a legacy renderer is called out with its count and the device it did render', async () => { + const ctx = makeCtx({ + ...ANALYTICS, + series: [ + ...ANALYTICS.series, + combo('prerender_ops', 'legacy_renderer', 'mobile', null, 37), + combo('prerender_ops', 'legacy_renderer', 'desktop', null, 5), + ], + }); + await load(ctx); + const text = draw(ctx).textContent; + + assert.match(text, /predate URL jobs/); + assert.match(text, /42 results/, 'counts sum across the device slot'); + assert.match(text, /desktop, mobile/, 'names what DID render, so a pod is recognisable'); + // The consequence is the part an operator cannot get from any other panel. + assert.match(text, /never being written/); +}); + +test('one legacy result is singular, and still shown — a single old pod is the whole finding', async () => { + const ctx = makeCtx({ + ...ANALYTICS, + series: [...ANALYTICS.series, combo('prerender_ops', 'legacy_renderer', 'mobile', null, 1)], + }); + await load(ctx); + assert.match(draw(ctx).textContent, /1 result was posted/); +});