Skip to content

aggregator(github-billing): billing items never validated — an impossible date discards the whole month's collection, negative amounts are stored as usage #2493

Closed

Description

@dezren39

Problem

aggregateBillingUsage in pm-stack/aggregator/src/sources/github-billing.ts:78-113 (the #1051 collector core, merged in PR #2236) validates almost nothing about the unified billing endpoint's items:

  • Date: the only check is typeof item.date === "string" && item.date.length < 10 skip (:81); the first 10 chars are taken verbatim (:82). "2026-13-99" and "2026-02-30" both pass.
  • Amounts: quantity, grossAmount, and netAmount are summed whenever typeof === "number" (:84, :99-100) — negative values pass; no finiteness or sign check anywhere in the file.

The two failure modes land in different places, and both are bad:

  1. One impossible date discards the whole month's collection. recordsFromBillingUsage (:149-168) stamps each day with dayTs(date) (snapshot-records.ts:32-34 — plain string interpolation, "2026-13-99T12:00:00Z"). Those records fail isSnapshotRecord (snapshots.ts:304, Date.parse is NaN), and the collector treats any invalid derived record as fatal: scripts/collect-github-billing.ts:84-86 throws and appends nothing — every good day in the month is lost because one item's date was malformed. A Feb-30 date is worse in the opposite direction: Date.parse rolls it over, so the record passes validation while carrying a literal nonexistent day as its ts (and dedupe key).
  2. Negative usage is stored as fact. A negative quantity/grossAmount/netAmount produces negative day aggregates, and the derived records (e.g. actions-minutes: -120, billing-gross: -0.96) pass isSnapshotRecord (finite is all it asks) and are appended to the snapshot store, corrupting the GitHub service's usage/billing series.

Sibling collectors in this repo all guard this: the Supabase exporter rejects negative/non-finite reads (parseDbSize, parseRequestCount in pm-stack/supabase-export/supabase-quota-export.mjs), and quota-gather-map.ts's num() is finite-gated. This file is the unguarded outlier.

Evidence (verified this run, main 7d1b081c)

Laptop probe (~/git/fleet/probe-2026-10-29-laneB-billing/probe2.ts, real github-billing.ts + snapshots.ts from a fresh clone at main, run with bun):

control (120 min, $0.96)            => day aggregates correct; 3 records, all isSnapshotRecord = true
quantity/gross/net all negative     => aggregates -120 / -0.96 / -0.96; records emitted with
                                       value -120 and -0.96, isSnapshotRecord = true for all (stored)
date "2026-13-99T00:00:00Z"         => day "2026-13-99" aggregates fine; records ts
                                       "2026-13-99T12:00:00Z", isSnapshotRecord = false for all
                                       => collector throws, whole month appended: nothing
date "2026-02-30"                   => records ts "2026-02-30T12:00:00Z", isSnapshotRecord = true
                                       (Date.parse rollover) — nonexistent day stored as-is
mixed good day + one month-13 item  => 6 records, only 3 valid — the good day is discarded with the bad one

Why it matters

This collector is the GitHub billing series' only writer. Either failure silently degrades it: a single malformed item from the endpoint (shape drift, a partial response, a future SKU/date format) zeros out a month of Actions/Codespaces/billing history with only a thrown error in a daily job log, and a negative row — which no downstream check rejects — permanently writes negative minutes/dollars into the snapshot store that quota views and reconciliation read.

Acceptance criteria

  • Items with a non-calendar date (month 13, Feb 30, non-date prefix) are rejected or skipped at aggregation time with the day/item identifiable — they never produce a record, and never take the month's good days down with them.
  • Negative (and non-finite, for direct API callers of the pure functions) quantity/grossAmount/netAmount values cannot produce negative aggregates or stored records; the chosen treatment (skip item vs skip field vs fail the run) is pinned by triage and documented in the file header.
  • The collector's all-or-nothing validation throw (collect-github-billing.ts:84-86) is reconsidered for whatever validation remains downstream: one bad item must not discard a month of valid records.
  • Regression tests in test/github-billing.test.ts cover: month-13 date, Feb-30 date, negative quantity, negative gross/net, and a mixed good+bad month that still yields the good days' records.
  • Well-formed items are unchanged: existing github-billing.test.ts expectations still pass.

Dedup: #1051 (closed) specified this collector; PR #2236 built it. #1540/#2483 concern the snapshot store validators (snapshots.ts pairs/limit/resetsAt) — this is the producer-side item validation in sources/github-billing.ts, which neither covers. Title/body searches for github-billing, aggregateBillingUsage, and "billing usage" returned no covering issue, open or closed.

Activity

  1. developing-today commented on Oct 30, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job, lane B (new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b, run 2026-10-30T01:20Z
    • Found via: filed this run
    • Task: Triage the github-billing item-validation gap (impossible dates discard the whole month's collection; negative amounts stored as usage) and route a fix packet.
    • Expectation: effort/xs — done when the acceptance criteria on the issue are met (bad items rejected/skipped without losing good days, no negative records, regression tests, well-formed items unchanged).
    • Links: probe on laptop ~/git/fleet/probe-2026-10-29-laneB-billing/probe2.ts (real modules, main 7d1b081)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 2 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

    Verdict: CONFIRMED at current main a11cfea3c (2026-11-01), with one behavior update vs the original filing.

    Evidence (fresh, this run):

    Layer pinned — aggregation owns validation (aggregateBillingUsage, github-billing.ts); the collector is NOT given per-item tolerance (its all-or-nothing throw stays as a backstop for genuinely new bug classes):

    1. Date: skip the item when isIsoDate(date.slice(0, 10)) is false (validate.ts strict primitive — the same one the store gates on).
    2. Numeric fields, per-field: quantity / grossAmount / netAmount contribute only when finite and ≥ 0, else 0 — extending the existing non-number fallback. Negative consumption is never a legitimate endpoint semantic, a negative/non-finite field must neither be stored nor poison the month, and zeroing one field does not discard the item's other valid fields.
    3. Surfacing: the skip/zero count is exposed from the aggregation layer and printed in the collector's existing summary line when > 0 — never swallowed.

    Regression tests named in the fix packet (test/github-billing.test.ts): month-13 skipped with the rest of the month intact, Feb-30 skipped, negative quantity / negative gross-net zeroed per-field, NaN/Infinity zeroed, mixed month derives a 100%-valid record set, well-formed months unchanged.

  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: claimed
    task: 5350 (repo-health-code-pilot, lane D)
    stage: pickup
    did: claimed small_fix; revalidated #2493 OPEN with no held/owned labels and no competing PR; aggregateBillingUsage at src/sources/github-billing.ts:78 matches the triage pin on main a11cfea.
    resume: implement the aggregation-boundary validation on a laptop worktree, test, open PR, merge when verified.

  9. added a commit that references this issue on Nov 1, 2026
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: done
    task: 5350 (repo-health-code-pilot, lane D)
    stage: fixed_merged
    did: PR #3445 merged (merge commit b09c9ce); this issue auto-closed. aggregateBillingUsage now skips items whose date is not a real calendar date (isIsoDate) and counts quantity/grossAmount/netAmount only when finite and >= 0 (isNonNegative). Targeted tests 10/10 on the laptop worktree; Codex review completed with no findings.
    resume: none — terminal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions