Publish presence and reachability over time - #39
Conversation
§10 asked for time-series endpoints and there were none. Two routes, both
hung off the game they describe:
GET /api/games/{id-or-slug}/presence?grain=hour|day&from=&to=
GET /api/games/{id-or-slug}/availability?from=&to=
Presence is served from the rollup rather than the raw table. §5.2 lets
retention drop raw partitions once they have been aggregated, so a series
read off presence_sample would quietly shorten as a deployment aged; the
rollup is the copy that outlives them, and the daily grain is kept for ever.
The silence is the hard part of publishing a series, and a JSON array is the
format most likely to flatten it. Three states go out as three things:
- counted bucket present, min/max/mean over the counted probes
- probed, no count bucket present, tallied separately, stats null
- never measured bucket ABSENT — never a zero row
countedSamples and uncountableSamples count probes and not players, and are
separate so that nothing downstream can add them together. Each response
carries the sentence that stops a consumer inferring the rest.
Both routes resolve through IGameQueries.FindAsync rather than reading a
store by id, which is load-bearing rather than tidy: FindAsync carries the
rule that keeps an unclaimed submission off every public surface, and a
series route that went to the store directly would be a way to read presence
for a game the listing refuses to show. Both honour §5.7 forever-redirects
and keep the sub-route and the querystring across them, because a series URL
is likelier to be in somebody's scheduled job than in a browser.
A window wider than we serve is a 400 rather than a silent clamp — truncating
would answer a different question and say so nowhere, and a consumer paging
through history would read short pages as the end of the record. Availability
spans are filtered by overlap and not containment, so the span running when
the window opened is in the answer though it began before it.
IPresenceSeries is a read port beside the store: publishing a series needs one
method, and the store also writes, rolls up, sets watermarks and drops
partitions. On the demo fixture it answers empty, because nothing there was
measured and a JSON body carries no demo banner to say so.
The API surface guard swept a hard-coded route list that these two were not
on, which is how the first draft published the word §5.8 forbids. Both routes
are on the list now and the copy says the limitation without the word.
1218 tests across five suites, Postgres exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (12)
WalkthroughThe PR adds presence and availability time-series API endpoints. It defines the presence read contract, response models, query validation, game resolution, dependency injection, fixture support, route-index entries, and API tests. ChangesSeries API
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to This PR adds the requested presence and availability time-series endpoints with documented behavior and reported passing build and test checks; no actionable merge-blocking risk remains beyond normal review. Sequence Diagram(s)sequenceDiagram
participant Client
participant SeriesEndpoints
participant IGameQueries
participant IPresenceSeries
Client->>SeriesEndpoints: Request presence series
SeriesEndpoints->>IGameQueries: Resolve game key
SeriesEndpoints->>IPresenceSeries: ForGameAsync(gameId, grain, from, to)
IPresenceSeries-->>SeriesEndpoints: Presence rollups
SeriesEndpoints-->>Client: PresenceSeriesView JSON
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
§10 asks for "time-series endpoints for presence and availability" and there were none. Two routes, each hung off the game it describes:
Presence comes from the rollup, not the raw table
§5.2 lets retention drop raw partitions once they've been aggregated, so a series read off
presence_samplewould quietly shorten as a deployment aged. The rollup is the copy that outlives them, and its daily grain is kept for ever.The silence is the hard part
A JSON array is the format most likely to flatten §5.4's three states into one — whatever a consumer is handed, it will draw a line through. So they go out as three distinguishable things:
min/max/meanover the counted probescountedSamplesanduncountableSamplescount probes, not players, and are kept separate so nothing downstream can add them together. Every response carries the sentence that stops a consumer inferring the rest.Two decisions worth flagging
IGameQueries.FindAsyncrather than reading a store by id. That's load-bearing, not tidiness:FindAsynccarries the rule that keeps an unclaimed submission off every public surface, and a series route that went to the store directly would be a way to read presence for a game the listing refuses to show.Availability spans are filtered by overlap, not containment, so the span running when the window opened is in the answer though it began before it — otherwise a game dark for three months answers "no spans", which reads as we never measured for exactly the game whose availability is most worth knowing.
IPresenceSeriesis a read port beside the store: publishing a series needs one method, and the store also writes, rolls up, sets watermarks and drops partitions. On the demo fixture it answers empty — nothing there was measured, and unlike a page a JSON body carries no demo banner to say so.One thing this PR found about itself
The API surface guard (
NothingOnThisSurfaceSaysTheWordWeDoNotUse) swept a hard-coded route list that these two routes were not on — which is precisely how the first draft of this branch published the word §5.8 forbids, in copy explaining that the word doesn't apply. Both routes are on the guard's list now, and the copy states the limitation without the word.Tests
8 new tests covering absent-vs-null, grain parsing and refusal, the window cap, a backwards window, the §5.7 redirect preserving sub-route and querystring, 404, and span overlap.
Not here
PeakDistinctEstimatestays unpublished. It's computed, stored and rolled up, but publishing a per-game unique-player estimate is a new claim about a game rather than a reformatting of one we already make, and the rollup's own docs are explicit that it is a peak and deliberately not a union. Worth a decision of its own rather than riding in on a plumbing PR.Independent of #38 — this reads the rollup store directly rather than the heatmap path, so the two don't conflict.
🤖 Generated with Claude Code
Summary by CodeRabbit