Relationships
#1589 A ledger read failure still renders as 'no activity': loadProfileScore swallows to an empty standing behind HTTP 200
Opened by keeb · 8/11/2026· Shipped 8/14/2026
Split out of #1586 (fixed in PR #1056), which asked for this as its request 1 and which deliberately deferred it.
What is wrong
loadProfileScore (lib/app/profile-score-reads.ts) ends with .catch(() => EMPTY_PROFILE_SCORE). Any ClickHouse ledger failure — timeout, breaker open, CLICKHOUSE_URL unset — therefore becomes an empty standing that is indistinguishable from an operative who has done nothing, and GET /api/v1/users/<name>/score-history serves it as HTTP 200 with count: 0.
That is exactly how #1586 hid for months: the endpoint burned its full 5s ceiling and answered 200 with zero entries, so it read as 'this operative has no activity' rather than 'this read failed'. The expensive read that made it fire constantly is gone, so the frequency is now low — the misreporting is not.
What #1056 already did
Only the tracing half. The span used to stamp Outcomes.success even when the catch fired; it now reports upstream_error with a score_reads.degraded attribute, so a degraded read is at least visible in Axiom. Nothing changed for a viewer or an API consumer.
What is still needed
Distinguish degraded from empty on the wire and on the page, the same way the leaderboard fill had to check pending before the empty-rows branch:
- The API should not answer 200 with a silently empty body when the ledger read failed. Either a 503 (matching the board endpoint's shape, which the island already retries on) or an explicit marker on the payload.
- The profile / activity page should say the score section is temporarily unavailable rather than rendering a legitimate-looking empty state. Note the deliberate asymmetry to preserve: the swallow exists so a ledger outage does not take down bio, badges, collectives, lab and extensions — that part is correct and should stay.
loadHeadlineTotalhas the same.catch(() => null)shape and should be considered in the same pass.
Worth deciding
Whether an empty standing and a failed read should be different types at the lib/app boundary rather than the same value, so a caller cannot accidentally treat one as the other — the current shape makes the bug easy to reintroduce anywhere else that consumes these helpers.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.