Metrics: no counter pairs route with bot; add route_bot_serve (route × bot × outcome)
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 47/100
- Issue 類型
- 功能
- 描述清晰度
- 基本清楚
- 活躍度
- 活躍
- 技術堆疊
- javascript, node.js
- 領域
- backend, observability
研究方向
Read src/http_handlers/bot_request.js at recordServeOutcome and the metric definitions in metrics.js, then check the positional-slot rules in METRICS.md. The issue proposes a route_bot_serve counter and mentions emission tests for recordServeOutcome; inspect those tests and confirm how status classes and outcomes are represented. Done means the new counter records route, bot, and outcome together while existing metric slots remain compatible.
由索引模型根據 Issue 內容生成。
描述
Summary
No metric the plugin emits has both the route and the bot, so "Googlebot on /browse/*: 91% from cache, 1.1% errors" can't be computed from counters. We're building a crawl-analytics dashboard for SEO teams on top of the plugin's metrics. Its main table is route × bot showing views, % from cache and errors, and today the only source for that table is the request sampler, which is an estimate at production sample rates.
Asking for one new counter, route_bot_serve (route × bot × outcome). We're happy to send the PR.
Why the current metrics can't answer it
Each metric has three positional slots (METRICS.md fact 4), and none pairs route with bot:
| metric | path | method | type |
|---|---|---|---|
bot_request |
host | botName | deviceType |
bot_serve |
source | cacheStatus | botName |
route_serve |
route | cacheStatus | deviceType |
bot_miss |
cause | route | botName (misses only) |
origin_fetch |
statusCode | reason | — (non-cache serves only) |
bot_miss has route × bot, but only for origin|miss serves. Cache serves, and every status code, never get both dimensions on one row.
The marginals can't be recombined: two traffic mixes that are mirror images of each other produce identical rows.
Reproduction
Any single node on 0.108.1 with two routes configured, e.g.:
'@harperfast/prerender':
ingress:
mode: forwarded
deviceTypeSource: header
routes:
- { match: prefix, path: '/ip/', queryParams: [] }
- { match: prefix, path: '/cp/', queryParams: [] }
Send 8 bot requests (mix A), wait for the analytics flush, then sum get_analytics for bot_serve by type and route_serve by path. Wait more than a minute, so the next window starts clean, then do the same for the mirror mix B. Script used:
mirror.mjs
// Two mirror-image traffic mixes against one node; print what bot_serve and route_serve record for each.
import { request, Agent } from 'node:https';
const APP = 19927, OPS = 9926, ORIGIN_HOST = 'host.docker.internal:8080'; // adjust
const AUTH = 'Basic ' + Buffer.from('admin:admin').toString('base64');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // self-signed sandbox certs
const agent = new Agent({ rejectUnauthorized: false });
const UA = {
Googlebot: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
GPTBot: 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot',
};
const send = (bot, path) => new Promise((ok, no) => request({ host: '127.0.0.1', port: APP, path, agent,
headers: { 'user-agent': UA[bot], 'x-forwarded-host': ORIGIN_HOST, 'x-forwarded-proto': 'http', 'x-device-type': 'desktop' } },
(r) => { r.resume(); r.on('end', () => ok(r.statusCode)); }).on('error', no).end());
const analytics = async (metric, start) => (await (await fetch(`https://127.0.0.1:${OPS}`, { method: 'POST',
headers: { 'content-type': 'application/json', authorization: AUTH },
body: JSON.stringify({ operation: 'get_analytics', metric, start_time: start }) })).json());
const sum = (rows, key) => { const o = {}; for (const r of rows) { const k = key(r); if (k) o[k] = (o[k] ?? 0) + r.count; } return o; };
async function run(name, mix) {
const start = Date.now();
let i = 0;
for (const [bot, route, n] of mix) for (let k = 0; k < n; k++) await send(bot, `${route}${100 + i++}`);
const want = mix.reduce((a, m) => a + m[2], 0);
for (;;) { // wait for the analytics flush
await new Promise((r) => setTimeout(r, 5000));
const bs = (await analytics('bot_serve', start)).filter((r) => r.type === 'Googlebot' || r.type === 'GPTBot');
if (bs.reduce((a, r) => a + r.count, 0) < want) continue;
const rs = (await analytics('route_serve', start)).filter((r) => r.path === '/ip/' || r.path === '/cp/');
console.log(`\n${name}: sent ${mix.map(([b, r, n]) => `${n}× ${b} → ${r}*`).join(', ')}`);
console.log(' bot_serve by bot :', JSON.stringify(sum(bs, (r) => r.type)));
console.log(' route_serve by route:', JSON.stringify(sum(rs, (r) => r.path)));
return;
}
}
await run('Mix A', [['Googlebot', '/ip/', 3], ['Googlebot', '/cp/', 1], ['GPTBot', '/ip/', 1], ['GPTBot', '/cp/', 3]]);
await new Promise((r) => setTimeout(r, 70000)); // next analytics bucket, so mix A can't leak into mix B
await run('Mix B', [['Googlebot', '/ip/', 1], ['Googlebot', '/cp/', 3], ['GPTBot', '/ip/', 3], ['GPTBot', '/cp/', 1]]);
Actual output (harper-pro 5.3.5, plugin 0.108.1, run 2026-11-07):
Mix A: sent 3× Googlebot → /ip/*, 1× Googlebot → /cp/*, 1× GPTBot → /ip/*, 3× GPTBot → /cp/*
bot_serve by bot : {"Googlebot":4,"GPTBot":4}
route_serve by route: {"/ip/":4,"/cp/":4}
Mix B: sent 1× Googlebot → /ip/*, 3× Googlebot → /cp/*, 3× GPTBot → /ip/*, 1× GPTBot → /cp/*
bot_serve by bot : {"Googlebot":4,"GPTBot":4}
route_serve by route: {"/ip/":4,"/cp/":4}
Expected: some metric that tells A (Googlebot mostly on /ip/) from B (Googlebot mostly on /cp/). Nothing does.
What we verified
All of this ran in a local sandbox with a fabricated origin, not on any customer cluster.
-
Setup: 4 replicating harper-pro 5.3.5 nodes, plugin 0.108.1, plus a puppeteer render worker.
-
The existing counters are exact. We replayed a seeded mix of 5,000 bot requests across 10 bots, 4 routes and 2 devices, with 91.8% served from cache. Summed across nodes,
hdb_analyticsmatched the known counts exactly on every check:bot_requestby botbot_serveby bot, and by sourceroute_serveby routeorigin_fetchby status, against the origin-served samples
So the counters are the right primitive; they just don't have this combination.
-
The sampler matches too, but only at rate 1. With
sample: { by: request, rate: 1 }, the sampled log gives bot × route × status exactly, and that check passed as well. At production rates it's an estimate, it's spread across per-node files rather than inhdb_analytics, and every reader has to page through every node's/prerender_admin/samples.
Proposal
One counter, recorded next to botServe and routeServe in recordServeOutcome (src/http_handlers/bot_request.js:244-245 in 0.108.1):
route_bot_serve counter path: route method: botName type: outcome
outcome = <source>:<status class> e.g. cache:2xx, origin:4xx, rendered:2xx
// metrics.js
routeBotServe: (route, botName, outcome) => server.recordAnalytics(true, 'route_bot_serve', route, botName, outcome),
// bot_request.js, recordServeOutcome
metrics.routeBotServe(route, request.botName, `${info.source}:${statusClass(resource.statusCode)}`);
- Closed sets only (METRICS.md rule 7):
- routes come from config, using the same label
route_servealready uses - bot names come from the registry plus
deriveUnknownBots - outcome is about 6 sources × 4 status classes
- routes come from config, using the same label
- Cost. It's one more counter bump, so the per-request cost is negligible (fact 2). The durable cost is active combos per node per period, roughly routes × active bots × outcomes in use. That's a few hundred on a typical site, the same order as
bot_serve. - Existing metrics stay as they are. We considered reusing
route_serve'sdeviceTypeslot for the bot, but that breaks the positional contract existing dashboards read. - Open question: which status to count.
recordServeOutcomeruns beforedeliverResource, soresource.statusCodeis what's available there. If you'd rather count conditional 304s the way the sampler does, the call could move after delivery and takeresponse.status.
Smaller asks (optional)
-
Crawl breadth per bot × route. The
crawlStatsHLL is per bot per day. Per bot × route would answer "distinct pages Googlebot crawled under/ip/". Happy to drop this if the sketch memory is a concern. -
Four crawlers for the registry (
src/configSchema.js, around line 3259):Amzn-SearchBotAmzn-UserMeta-WebIndexerfacebookexternalhit
None of the four is in 0.108.1.
deriveUnknownBotsalready gives them usable names, so nothing breaks, but registering them would pin their display names and bot classes.
Questions
- Is the counter acceptable, and does
route_bot_servefit your naming? - Would you take a PR from us? It would add unit tests in the style of the existing
recordServeOutcomeemission tests. Our 4-node replay would also check the new metric exactly against known counts. - Roughly which release could it land in? Until then we'll show sample-based estimates, labelled as estimates.
- 主要語言
- JavaScript
- 星號
- 0
- 分支
- 0
- 平均合併
- 4 小時 47 分鐘
- 30 天內合併 PR
- 8
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 沒有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
HarperFast/prerender-plugin 的其他 Issue
-
enhancement
難度 3/5 1-2 天 新手友好度 42/100
HarperFast/prerender-plugin#259 ·
維護者通常 1 天內回覆
-
enhancement
難度 5/5 一週以上 新手友好度 25/100
HarperFast/prerender-plugin#244 ·
維護者通常 1 天內回覆
-
enhancement
難度 5/5 一週以上 新手友好度 30/100
HarperFast/prerender-plugin#242 · 1 則留言 ·
維護者通常 1 天內回覆
-
enhancement performance
難度 5/5 一週以上 新手友好度 38/100
HarperFast/prerender-plugin#235 ·
維護者通常 1 天內回覆
-
enhancement performance
難度 5/5 一週以上 新手友好度 38/100
HarperFast/prerender-plugin#233 ·
維護者通常 1 天內回覆
查看 HarperFast/prerender-plugin 的全部 Issue
相似的 Issue
-
ai core enhancement good first issue testing
難度 2/5 1-3 小時 新手友好度 88/100
維護者通常 1 天內回覆
-
bug
難度 2/5 1-3 小時 新手友好度 68/100
-
bug
難度 2/5 1-3 小時 新手友好度 75/100
-
good first issue help wanted
難度 2/5 1-3 小時 新手友好度 88/100
-
cli: `studio --request <missing file>` answers with a raw ENOENT/EISDIR instead of a clear error可能已有人在做 關聯的 PR 仍在進行中或已合併。 未關閉bug good first issue help wanted
難度 2/5 1-3 小時 新手友好度 88/100
laaielichter/Ai-edit-studio#81 ·
維護者通常 1 天內回覆