fix(telemetry): usage readout grouped by a rebuilt CASE — group by the label instead (#2663 root cause)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 39s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 39s
CI's new integration tests reproduced the outage on a clean database and named the half: the writes land fine, and usage_for_notes fails on EVERY call. The GROUP BY rebuilt the ambient case() expression, and asyncpg's expanding IN-parameters give each instance its own bind names — so Postgres sees a SELECT expression the GROUP BY doesn't cover and rejects the query with a GroupingError, which the old code swallowed into zeros. One labelled expression, grouped by its label. The deployed table has been accumulating events all along; history appears as soon as this deploys. Also: the two hook-execution tests now run with the real PATH and skip when the hook's tools are absent (the restricted-PATH convention next door is for silence contracts, where empty-for-the-wrong-reason still passes) — and the unit lane installs jq so 'skip' never quietly becomes 'nowhere'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,18 @@ async def usage_for_notes(note_ids: list[int]) -> dict[int, dict]:
|
||||
if not ids:
|
||||
return out
|
||||
|
||||
# Classified in SQL so the group count stays small: per note we get at most
|
||||
# (surfaced-ranked, surfaced-ambient, pulled) rather than one row per
|
||||
# distinct source. ONE labelled expression, grouped by its label — a second
|
||||
# case() instance in GROUP BY renders with its own expanding-IN bind names
|
||||
# under asyncpg, so the database sees two DIFFERENT expressions and rejects
|
||||
# the query with a GroupingError. That rejection was swallowed, which is
|
||||
# how every counter read zero in production while the writes were landing
|
||||
# fine (#2663).
|
||||
ambient = case(
|
||||
(NoteUsageEvent.source.in_(AMBIENT_SOURCES), True),
|
||||
else_=False,
|
||||
).label("ambient")
|
||||
try:
|
||||
async with async_session() as session:
|
||||
rows = (
|
||||
@@ -195,22 +207,13 @@ async def usage_for_notes(note_ids: list[int]) -> dict[int, dict]:
|
||||
NoteUsageEvent.event,
|
||||
func.count().label("n"),
|
||||
func.max(NoteUsageEvent.created_at).label("last_at"),
|
||||
# Classified in SQL so the group count stays small: per
|
||||
# note we get at most (surfaced-ranked, surfaced-ambient,
|
||||
# pulled) rather than one row per distinct source.
|
||||
case(
|
||||
(NoteUsageEvent.source.in_(AMBIENT_SOURCES), True),
|
||||
else_=False,
|
||||
).label("ambient"),
|
||||
ambient,
|
||||
)
|
||||
.where(NoteUsageEvent.note_id.in_(ids))
|
||||
.group_by(
|
||||
NoteUsageEvent.note_id,
|
||||
NoteUsageEvent.event,
|
||||
case(
|
||||
(NoteUsageEvent.source.in_(AMBIENT_SOURCES), True),
|
||||
else_=False,
|
||||
),
|
||||
ambient,
|
||||
)
|
||||
)
|
||||
).all()
|
||||
|
||||
Reference in New Issue
Block a user