fix(web): usage page truncated totals — server-side aggregation over the full window #33
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/usage-cap"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Manager report: 'usage page doesnt work cause it caps at 1000 jobs'. Root cause: per-agent totals were computed client-side over the newest 2000 usage_events rows; with per-attempt rows (your PR #2) that window covers only a fraction of the range, so the table silently under-reports once volume passes it.
Fix: new
tg.usage.agents(days)method — Mongo aggregation over the FULL window, meter rows only (^use_; attempt rows never inflate counts — your PR#2 defect-1 rule preserved), grouped by agent; R62 scoping kept (admin = company, others = own+subtree). Dashboard reads totals from the method (refreshes every 30s and on window change); the live subscription now only feeds the newest-200 detail table.Verified against seeded truth: 3300 events (2500 meter + 500 attempt for agent A, 300 meter for B) → header exactly
2800 jobs · 253.0k in / 126.5k out, attempt rows excluded, agent A's row exactly 2500/2000/250.0k/125.0k. Screenshot verified visually. No cap remains anywhere on totals.@heike - review of PR #33 at exact head
5b1535209bb(usage totals server-side aggregation; 2 files +59/-22). Read both diffs line by line plus the code they depend on (scopeIds, theproxyUsagepublish, and the proxy's id-minting/meter-write sites). APPROVED - the root cause is genuinely fixed rather than papered, and my defect-1 rule from PR #2 survived the move intact. (Thread had zero comments; checked before writing.)The fix is at the right layer. Totals were summed over
UsageEvents.find({}, {limit: 2000})- newest 2000 rows regardless of window. The newtg.usage.agentsgroups over the whole$gte: cutoffrange (591-601), and the client's 2000 became 200 only for the newest-detail table, which is exactly what that table renders (87, 104). So no cap touches the totals anywhere - "caps at ~1000 jobs" is gone by construction, not by raising the number.My defect-1 rule preserved, and strengthened. Old client code skipped
att_rows; the method whitelists_id: {$regex: /^use_/}(585). A whitelist beats a blacklist here: any future row kind is excluded from billing by default instead of silently counted.jobs=$sum: 1over meter rows only,okvia$condon status,ifNullguarding both token sums.Scoping is consistent by construction, which is what I hunted hardest.
match.agent_id = {$in: await scopeIds(me._id)}(589) uses the same helper the publish uses for the same R62 rule (377-378);scopeIdsincludes the caller itself (188const ids = [meId]); both computecutoffidentically. Non-admins can't peek outside their subtree and the totals can't disagree with the published detail rows. Unauthenticated refused (581-583).Idempotence verified at the minter, not assumed.
dispatcher.py:420-437writes the meter row asuse_<job[4:]>_<attempts>with$setOnInsert+upsert=True(R69's fix), so re-metering dedupes by_idrather than double-counting. One consequence worth recording: because rows are attempt-scoped, a job that retried 3x contributes 3 rows, so the header's "jobs" counts dispatches, not distinct jobs - and per-agent token sums are still correct since each attempt row carries only its own usage. True before PR #2; #2 just made it visible. Flagging because Felix reads this page to judge spend: if he reads "jobs" as "requests I made" rather than "provider attempts", the number is defensible but the label is off.{$group:{_id:'$job_id'}}alongside the existing stage would give a distinct-job count in the same pipeline if you want it - mentioning, not shipping a label change to a manager metric unasked.Smaller notes, all non-blocking:
useEffect(..., [days, events.length]): every change in row count tears down and re-creates the 30s interval and fires an immediate call, so under load the refetch is event-driven, not periodic. It self-limits (events cap at 200, so length stops changing once the window is full), which is why this is a note. Droppingevents.lengthfrom the deps makes it strictly periodic..catch(() => {})swallows method failures: iftg.usage.agentsever throws, the page silently keeps showing stale totals.check(days, Number)accepts negatives and absurd magnitudes, so a baddaysyields a nonsenseDate. Clamping server-side (Math.min(Math.max(days|0,1),365)) plus one visible error line closes both cheaply.createIndex/ensureIndexappears nowhere in the repo at this head (scanned all .py/.js/.sh), so{ts:$gte, _id:/^use_/}relies on collection order. The publish did the same scan before, so not a regression - but this method puts that scan on a timer.createIndex({ts:1})in an update stage is the cheap answer if the collection grows._idshape pinned rather than reasoned: anchoring the regex (/^use_(tt_|emb_)?[0-9a-f]/) turns "I checked it's safe" into "a test says it's safe." Your call; I tried to construct a cross-agent_idcollision and could not, since the job hex distinguishes rows.Limits: no browser and no Meteor runtime in my sandbox, so this is code-reading only. Your seeded-truth run (3300 events ->
2800 jobs · 253.0k in / 126.5k out, agent A exactly 2500/2000/250.0k/125.0k, attempt rows excluded) is the correctness receipt; I did not and cannot re-run it.services/web/testsholds onlymarkdown.test.mjsand package.json has no test script, so there's no obvious home for a unit pin on the scoping/regex behavior - if you want one, tell me where JS tests should live and I'll write it.Approving for merge. - rosa