Draft: AR namespace statistics: Client method and value object
What does this MR do and why?
Adds a client method and value object for namespace statistics. Nothing calls it yet — the GraphQL statistics field is a separate task.
ArtifactRegistry::Client#namespace_statistics(slug:) issues GET /api/v1/:slug/statistics and returns an ArtifactRegistry::NamespaceStatistics with repositories_count and deduplicated_size_bytes.
Design decisions
- Two fields exposed, not four. The response carries
components_countanddownloads_counttoo, but neither has a consumer yet. Adding them later is additive — the value object reads keys off the parsed body, so no existing code changes. - Negative
deduplicated_size_bytespasses through. The contract declares nominimum, so underflow is possible until reconciliation. Clamping in the client would hide drift from the consumer (the header renders negatives as zero per S04). - Missing required fields raise
UnavailableError. Both fields arerequiredin the contract; an absent key signals something between the monolith and AR is rewriting the response. This differs from#version_statistics, whose single figure is nullable. 404returnsnilwith logging. Follows the existing#repositoriespattern. A501raisesUnavailableError(unlike#version_statistics) because this route is fully implemented, not a placeholder.- Auth uses
user_request. The per-user token is sent asAuthorization: Bearer, matching every other/api/v1read.
Files changed
ee/lib/artifact_registry/client.rb: 1 public method, 1 path builder, 1 contract predicate (+21)ee/lib/artifact_registry/namespace_statistics.rb: new value object (+17)ee/spec/lib/artifact_registry/client_spec.rb: 24 new examples (+201)ee/spec/lib/artifact_registry/namespace_statistics_spec.rb: new spec, 8 examples (+77)
Total: 316 insertions, 0 deletions, 4 files.
Specs cover: request path, auth token handling, response parsing (full four-field body, two exposed fields), error mapping (401/403 → AuthorizationError, 404 → nil, 429/500/503/501 → UnavailableError, 400 → ApiError), and edge cases (missing fields, array body, 204, blank slugs, percent-encoding).
This ships behind the artifact_registry_ui feature flag (disabled). No changelog, no database change, no user-visible change. All files under ee/.
References
- Implements: wi!628181
- Parent: wi!627138
- Epic: wi!22321
- Spec amendment (merged): AR !2195
- Monolith S04 spec (
## Ruby client, Phase 2namespace_statistics): link - Monolith S10 spec (second consumer): link
- Go-service endpoint work (merged): wi!316
- OpenAPI contract: v1.yaml
Screenshots or screen recordings
Not applicable — client-layer change with no UI or GraphQL surface.
How to set up and validate locally
-
Run the specs:
bundle exec rspec ee/spec/lib/artifact_registry/client_spec.rb bundle exec rspec ee/spec/lib/artifact_registry/namespace_statistics_spec.rbConfirmed:
869 examples, 0 failures(client) and8 examples, 0 failures(value object). Filtering the client spec to-e '#namespace_statistics'gives24 examples, 0 failures. -
Run RuboCop:
bundle exec rubocop ee/lib/artifact_registry/client.rb ee/lib/artifact_registry/namespace_statistics.rb ee/spec/lib/artifact_registry/client_spec.rb ee/spec/lib/artifact_registry/namespace_statistics_spec.rbConfirmed:
4 files inspected, no offenses detected. -
Drive the method against a running Artifact Registry.
Environment
A local Artifact Registry built from origin/main on http://localhost:8080, verifying the per-user JWT the GDK mints.
- GDK organization:
default, user:root - Artifact Registry namespace slug:
gdk-local-test - That namespace holds 4 repositories (one docker, one maven, one hosted npm, one remote npm)
The monolith needs this under development: in config/gitlab.yml. gdk reconfigure overwrites the block, so re-add it after reconfiguring:
development:
artifact_registry:
api_url: http://localhost:8080Artifact Registry needs this, pointing keys_url at Workhorse on :3333 (:3000 is the HTTP router and 404s the JWKS path), while issuer.url stays :3000 because that is the iss claim it checks:
auth:
token_exchange:
issuer:
url: http://gdk.test:3000
keys_url: http://gdk.test:3333/oauth/discovery/keys
expected_audiences:
- gitlab-artifact-registryDriven with bundle exec rails runner, resolving the slug through ArtifactRegistry::NamespaceMapping and building the client with organization.artifact_registry_client(current_user: user).
Raw response
What Artifact Registry actually returned for GET /api/v1/gdk-local-test/statistics (HTTP 200):
{
"repositories_count": 4,
"deduplicated_size_bytes": 4809,
"components_count": 3,
"downloads_count": 0
}That confirms the four-field contract body. ArtifactRegistry::NamespaceStatistics reads only the two fields with a consumer today.
Scenarios
| # | Scenario | Result |
|---|---|---|
| 1 | namespace_statistics(slug: 'gdk-local-test') |
ArtifactRegistry::NamespaceStatistics with repositories_count = 4, deduplicated_size_bytes = 4809; does not respond to components_count or downloads_count |
| 2 | Cross-check against #repositories |
4 nodes returned, matching repositories_count |
| 3 | Unknown slug no-such-namespace |
nil, plus exactly one error-tracking event: ArtifactRegistry::Client::ApiError carrying {slug: "no-such-namespace", status: 404} |
| 4 | Blank slug ' ' |
ArgumentError: slug is required, no HTTP call made |
| 5 | slug: nil |
same ArgumentError |
| 6 | Slug needing percent-encoding, 'a b/c' |
encoded, Artifact Registry answered 404, method returned nil |
| 7 | No current_user (service client) |
ArgumentError: current_user is required for a per-user request |
| 8 | User outside the organization | no token minted, blank-credential guard raised ArtifactRegistry::Client::AuthorizationError: No Artifact Registry credential was obtained |
Full captured output
org=default user=root slug=gdk-local-test api_url=http://localhost:8080
--- raw AR response body (what the contract actually sends) ---
HTTP 200
{
"repositories_count": 4,
"deduplicated_size_bytes": 4809,
"components_count": 3,
"downloads_count": 0
}
--- 1. happy path: namespace_statistics(slug:) ---
class = ArtifactRegistry::NamespaceStatistics
repositories_count = 4
deduplicated_size_bytes = 4809
does NOT expose components_count/downloads_count: true
--- 2. cross-check repositories_count against #repositories ---
repositories page size = 4
statistics count = 4
--- 3. unknown slug -> nil (404 mapped, one error-tracking event) ---
result = nil
events = 1
ArtifactRegistry::Client::ApiError: {:slug=>"no-such-namespace", :status=>404}
--- 4. blank slug -> guarded before any HTTP call ---
RAISED ArgumentError: slug is required
--- 5. nil slug -> guarded ---
RAISED ArgumentError: slug is required
--- 6. slug needing percent-encoding -> encoded, AR answers 404 -> nil ---
result = nil
--- 7. no current_user -> service client refuses to make a user request ---
RAISED ArgumentError: current_user is required for a per-user request
--- 8. non-member user -> no token minted, blank-credential guard ---
outsider=petrina_koepp member=false
RAISED ArtifactRegistry::Client::AuthorizationError: No Artifact Registry credential was obtained
DONENot reproducible against a live service
Three of the design decisions above stay covered by specs only, because a healthy Artifact Registry cannot produce the input:
- Missing required field to
UnavailableError. Artifact Registry always sends bothrepositories_countanddeduplicated_size_bytes. 501toUnavailableError. The route is fully implemented, so it never answers501.- Negative
deduplicated_size_bytespass-through. The local namespace has no accounting underflow.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.