Skip to content

aggregator(snapshots): isSnapshotRecord never checks limit/resetsAt — a numeric resetsAt crashes the service history chart, a non-numeric limit NaNs it #2483

Closed

Description

@dezren39

Summary

isSnapshotRecord (pm-stack/aggregator/src/snapshots.ts:26-42) — the structural validator every snapshot-store read and append gates on — never checks the record's two optional fields, limit and resetsAt (model: pm-stack/aggregator/src/model.ts:1266-1267, limit?: number | null, resetsAt?: string | null). A stored JSONL record carrying either field in the wrong type passes validation, is returned by SnapshotStore.readAll/byService, and then breaks the service-detail history chart:

  • resetsAt as a number crashes the chart. resetMarkers (src/derive.ts:975-991) treats any truthy resetsAt as a string: Date.parse(prev.resetsAt) coerces a number to a string and can parse it as a date (e.g. 2026 → 2026-01-01), so the marker is emitted with the number as its ts/resetsAt. historyChartHtml (src/render.ts:1869-1875) then calls m.resetsAt.slice(0, 10) on that number → TypeError: m.resetsAt.slice is not a function. The call site is the /quota/service/<id> route (src/server.ts:527, renderServiceDetail → historyChartHtml), which has no try/catch around the render, so one poisoned history record 500s that service's detail page on every load until the record ages out of the store.
  • A non-numeric limit silently NaNs the chart. historyChartHtml (src/render.ts:1866) folds r.limit ?? 0 into the Math.max(...) that computes vmax; an object/array/non-numeric-string limit makes vmax NaN, so every chart coordinate renders as NaN — the chart is garbage with no error anywhere.

Evidence (verified this run, main 1ddc7720)

Laptop probe ~/git/fleet/probe-2026-10-29-laneA-snapreset/ (verbatim aggregator src/ copied from current main; sha256 of snapshots.ts/render.ts/derive.ts identical on sandbox and probe), Bun, driving the real modules:

  • isSnapshotRecord({...valid, resetsAt: 2026}) → true; limit: "not-a-number" → true; limit: {x:1} → true.
  • resetMarkers([record(resetsAt: 2026, ts 2025-12-01), record(ts 2026-11-01)], "m") → [{ts: 2026, resetsAt: 2026}].
  • historyChartHtml(same pair, "m") → throws TypeError: m.resetsAt.slice is not a function.
  • Control (string resetsAt: "2026-06-01T00:00:00Z"): chart renders, marker present.
  • historyChartHtml with limit: {x:1}: output contains NaN coordinates.
  • Reachability: live source loads history via SnapshotStore.byService() (src/sources/live.ts:799) into usage.history, keyed by service id — exactly what the service route renders.

Why it matters

The snapshot store is append-only history written by several jobs and a backfill; the validator is the only boundary between an on-disk JSONL line and the renderers, and it already type-checks every other field (including finiteness for scalar value). One malformed line — a writer emitting a numeric epoch-year or a string limit — permanently breaks one service's detail page (crash) or its history chart (NaN), with the failure surfacing far from the bad record.

Acceptance criteria

  • isSnapshotRecord rejects a record whose limit is present and not a finite number or null, and whose resetsAt is present and not a string that Date.parse accepts (or null).
  • Regression tests: a numeric-resetsAt record and an object-limit record each fail validation / are skipped by readAll; the string-resetsAt + numeric-limit control still renders a chart with its reset marker.
  • No change to the on-disk format, the dedupe key, or behaviour for records whose optional fields are absent or well-typed.

Notes

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 (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run, 2026-10-29 19:35 CDT
    • Found via: filed this run
    • Task: Filed aggregator(snapshots): isSnapshotRecord never checks limit/resetsAt — a numeric resetsAt crashes the service history chart, a non-numeric limit NaNs it #2483 — isSnapshotRecord never checks limit/resetsAt; verified on laptop probe with the real snapshots/derive/render modules (numeric resetsAt crashes historyChartHtml, object limit NaNs it). Enqueued as a triage packet for the queue worker.
    • Expectation: effort xs — triage pins validator-only vs validator+renderer hardening, then routes a small_fix.
    • Links: probe ~/git/fleet/probe-2026-10-29-laneA-snapreset/ (laptop); evidence body in hidden_files/issue-code-snapshot-resetlimit-body.md
  4. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Closing as fixed by PR #3035 (merged 5411129, queue task 4006 slice 1): isSnapshotRecord in snapshots.ts now checks limit/resetsAt — a non-finite/negative limit or an impossible resetsAt date refuses the record, expressed with the Phase 1 validate.ts primitives (isFiniteNumber / isIsoDate). Probe coverage landed in the snapshot-writers suite; laptop verification recorded on PR #3035 (targeted suites 43 pass; full aggregator suite 676 pass / 7 pre-existing page-manifest fails reproduced on clean main).

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)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/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