feat(managementapi): format statistics (S17 Phase 8 plan: 7/13)
Why
The four per-format statistics routes answer 501. Step 3 registered them as
placeholders and Step 5 landed the aggregate they read, so this fills the shells:
GET /api/v1/:slug/{docker,oci,maven,npm}/statistics serves the format's active
repository count and its three counter sums.
The consumers are the monolith UI's per-format figures.
This was built stacked on chore(datastore): repositories aggregate (S17 P... (!1970 - merged) • Hayley Swimelar • 19.4, which owns
RepositoryStore.AggregateRepositoryCounters and the RepositoryAggregator
seam it reads. It is rebased onto main after that merged, so the diff here is
Step 7 alone.
What is not obvious from the diff
-
The format is a construction-time parameter, not a path read. The four routes are separate literals because a
{format}wildcard at that position overlaps/api/v1/:slug/repositories/statistics, andnet/http.ServeMuxpanics at registration on exactly that pair. One handler function serves all four, and each registration binds its owndatastore.RepositoryFormat*constant, so the four are distinct handler values. An unmapped constant has no wire string and would serialize"format": "", so construction panics on it, matchingrequireDeps's boot-time failure. -
An unknown format segment gets the bare mux's
text/plain404, not an envelope. The subtree registers no fallback deliberately: that is how the contract sweep tells a declared-but-unserved operation from a served one.server.ResponseInterceptorMiddlewarerewrites it to the S01 envelope on the served chain, so the handler tests assert what the tree serves directly. -
downloads_countis live for Maven only. Maven's delivered archiveGETwritesrepositories.downloads_countthroughMavenRepositoryStore.BumpRepoDownloadCount; npm, docker, and container have no writer until S18. So the maven route's figure moves in production and the other three read zero.docs/specs/S17-rest-management-api.md's S18 dependency row still calls that bump a no-op stub, which has been wrong since the hosteddownloads_countbump gained its delivery gate. Thedocs(specs)correction is tracked in S17 spec: the S18 row still calls BumpRepoDownl... (#975) • Hayley Swimelar. -
size_bytessums per-repository footprints and does not dedup across them. It is the one byte figure on this surface that dedups at a narrower scope than its own response, per the spec's Size semantics. -
The sums serve as summed, negatives included.
CounterDrainStore.ApplyRepoDeltascommits a decrement past zero rather than clamping, sorepositories.artifacts_countandsize_bytesare negative-capable between a dropped increment and the reconciliation pass that corrects them from source. The spec defines this scope's three figures as "Sums of therepositoriescounters over the format's repositories", and a floored sum is not that sum, so the handler passes them through.repositories_countis aCOUNTanddownloads_counthas only an incrementing writer, so neither can arrive negative anyway.The contract agrees, since chore(api): drop statistics floors the counter ... (!1995 - merged) • Hayley Swimelar • 19.4 dropped
minimum: 0from the figures the counter pipeline can drive negative and kept it on the ones it cannot, closing Statistics contract declares minimum: 0 that no... (#921 - closed) • Unassigned. That merged and this branch is rebased onto it, so the underflowed response is validated against the schema like every other success case, as a row ofTestStatisticsFormat_ResponsesMatchOpenAPIContract.The monolith consumer cannot hit a stale floor: its AR client is hand-written (
ee/lib/artifact_registry/onmaster, no generated code, no response-schema validation) and has no statistics reader yet, so an underflow window is a displayed negative, not a decode failure. -
The format-filtered plan shape is pinned in the parent MR, by
TestAggregateRepositoryCountersStmt_FilteredFormRidesThePartialFormatIndex. This MR is that statement's first production caller with a non-nil format. -
No new
Depsfield and nowire_management.gowiring.RepositoryAggregatorarrived with Step 5, so theDepsseam-count pin inhandler_test.gostays atmain's 86. The plan's Step 7 Files list nameswire_management.goanyway; it appears here only to delete the three anchor comments this step consumes.
Shared seams: what conflicts with this branch, and how it resolves
Step 6 (feat(managementapi): namespace statistics (S17 ... (!2001 - merged) • Hayley Swimelar • 19.4) and Step 19
(feat(managementapi): maven version stats (S17 P... (!2021 - merged) • Hayley Swimelar • 19.4) merged on 2026-08-28 and this branch
is rebased onto both, so the conflicts this section used to resolve are history.
Re-measured with git merge-tree --write-tree at this branch's 9f0a75dff and
main at b0a19460a:
- feat(managementapi): container repo stats (S17 ... (!2030) • Hayley Swimelar • 19.4 (Step 10,
ff24856a9) merges clean against this branch, againstmain, and against this branch's base. - feat(managementapi): npm version stats (S17 Pha... (!2035) • Hayley Swimelar • 19.4 (Step 20,
aebc05d72) has since rebased ontomain, so the 16-file list this bullet used to carry is gone. It conflicts against this branch oninternal/managementapi/contract_routes_test.goalone and merges clean againstmainand against this branch's base, so the conflict is this pair's rather than something a rebase of either side clears. Both sides delete adjacent rows ofcontractPendingOperations, this branch the fourget<Format>Statisticsrows and Step 20 thegetVersionStatisticsnpm row, so the resolution takes both deletions. - refactor(managementapi): bind every route (S09 ... (!2062 - merged) • David Fernandez • 19.4 (S09 Enforcement step 16,
fc7d72fae) conflicts against this branch on one region ofinternal/managementapi/handler.goand merges clean againstmainand against this branch's base. It rewrites how the four format rows register (routes.handleon abindingMux, each row carryingrequirePermission(authz.ActionReadRepository, scopeNamespace)), and this branch rewrites what they register (handleFormatStatisticswith the aggregate and the row's own format constant), so the resolution keeps that MR's rows with this branch's arguments and comment. The two region-level one-sided resolutions each fail to compile:muxis gone once registration moves to the binding mux, and the zero-argumenthandleFormatStatistics()is gone once this branch lands. That MR also carriesTestPackageSources_RegisterEveryRouteThroughTheBindingHelper, which walks the package's non-test sources for a rawmux.Handlebypass, so a resolution that reinstates one is caught there too.
The durable seam is the pair every statistics step edits by design: the
servedStatisticsScopes opt-out map in handler_test.go and the
pending-operations map in contract_routes_test.go. Where two in-flight steps'
rows land adjacent, take both sides' edits rather than either alone. A one-sided
resolution fails loudly rather than silently, because a sweep drives each map:
at 9f0a75dff, dropping this branch's four servedStatisticsScopes rows fails
TestHandler_StatisticsRoutes_Return501WithEnvelope on all four routes, and
restoring its four contractPendingOperations rows fails
TestContractOperations_AreRegisteredAndServed on all four at
contract_routes_test.go:323, with a message naming the fix. So this section
saves whoever rebases a re-measurement, not a defect.
The handler.go statistics region banner is settled by this diff: main
carries Step 6's wording, stale since this scope landed, and this branch
replaces it with "the repositories aggregate the statistics scopes share",
which stays true whichever step lands next.
Whoever rebases second should re-measure against these SHAs rather than trust the verdicts.
Diff size
810 reviewable LOC (783 added, 27 removed) across 11 files, none vendored,
generated, or binary, so it is over the 500 ceiling.
Split by group: production 62 added / 21 removed across 4 files
(statistics_format.go, handler.go, statistics_resources.go,
wire_management.go), tests 721 added / 6 removed across 7 files.
Not split: the tests land with the implementation they
constrain, and separating them would leave the test-first discipline and the
"every MR includes tests for the code it introduces" guardrail unmet on
whichever half merged first.
Test plan
go test ./internal/managementapi/ -count=1and-raceare green, as is the fullgo test ./....go test -tags=integration ./internal/managementapi/ -run FormatStatisticsis green against a real Postgres.golangci-lint runand the integration-tagged run (--build-tags=integration --max-same-issues=0 --max-issues-per-linter=0 --uniq-by-line=false) both report 0 issues oninternal/managementapi.- Each assertion was checked against the mutation it exists to catch: passing
nilfor the format rolls up every format and fails the per-route table; serving one format's figures on another's route fails the docker/oci case; omitting a zero figure fails theassert.JSONEqon the empty format; and the unknown-segment 404 carries a served-route control, without which it passed vacuously while the whole subtree was unimplemented; and re-adding a floor to either summed figure fails the verbatim-negatives case. - The two round-2 tests were checked the same way: deleting the
errors.Isclassification fromlogAndWriteInternalErrorfails both cancellation rows, and dropping the schema'sdownloads_countfloor fails the contract-rejection row. - The integration fixture gives all four formats and all three kinds a live row
plus one soft-deleted row, with per-row counters whose subset sums are unique,
and a
require.Positiveguard fails the suite if a future format arrives without one.
No e2e catalog scenario is added or affected: docs/testing/ holds the four
protocol-client catalogs and none of them reads statistics.
Plan reconciliation
Touched but not in the step's Files list: handler_test.go (the
servedStatisticsScopes opt-out, above) and statistics_format_internal_test.go
(a white-box file for the construction panic, which is unreachable from the
external test package).
Related to #316