Draft: Show unresolvable locations in Credits usage events
What does this MR do and why?
The GitLab Credits user event table renders a blank Location cell whenever an event's project or group cannot be resolved — either because it was deleted or because the viewer cannot see it. The historical billing event still carries the reference, so a valid credit charge ends up looking unattributable, which costs billing transparency and pushes users to support.
This MR falls back to unlinked Project not found (ID: 358215) text whenever
location is null but locationId is present.
Why "not found" rather than "deleted"
Deleted and inaccessible records render identically. location is a
Project | Group union with type-level authorization, so GraphQL nulls it out for a
viewer who lacks access before any resolver could observe why — and merging the two
states is what keeps the table from disclosing whether a private project exists.
"Not found" is GitLab's established wording for exactly that ambiguity, matching
CiCatalog|Component ID not found, or you do not have permission to access component.
and the product-wide convention of answering 404 rather than 403.
Also fixed
Events whose namespace_id points at a personal namespace previously rendered blank
as well: BatchModelLoader.new(Group, …) applies the Group STI scope, which can
never match a Namespaces::UserNamespace. Those rows now get the fallback too.
Depends on
!248960 (merged) adds the locationId
field. That MR must be merged and deployed before this one, per the
multiversion compatibility guidance
— @gl_introduced(version: "19.3.0") on the query field covers GitLab Self-Managed,
but does not mitigate the GitLab.com canary window.
This branch is currently based on the backend branch so graphql-verify can resolve
the field; it will be rebased onto master once the backend lands.
Out of scope
- Immutable path snapshot (iteration 2 in the issue). Capturing the project path at event ingestion needs coordinated changes across Usage Billing, CustomersDot and the billing event emitter, plus decisions on retention, renames/transfers, and whether paths may be exposed after deletion. This MR is the resilient-fallback iteration, which works for existing historical rows without any of that.
- Events emitted without any location. Rows carrying neither a
project_idnor anamespace_idkeep rendering an empty cell — there is no attribution to show. That case is tracked in #591437.
References
Screenshots or screen recordings
| Before | After |
|---|---|
![]() |
![]() |
How to set up and validate locally
yarn storybook:start- Open the
ee/usage_quotas/usage_billing/users/show/app→ Default story. - The events table shows a linked project, an empty cell for a location-less event,
and
Project not found (ID: 358215)for the unresolvable one.
To exercise the real page, stub the CustomersDot client in a development-only
initializer so get_events_for_user_id returns an event whose projectId does not
resolve, then visit
/admin/gitlab_credits_dashboard/users/<username>.
MR acceptance checklist
- Jest specs cover all four cases: resolvable project, unresolvable project, unresolvable group, and a genuinely location-less event.
-
locale/gitlab.potregenerated for the two new externalized strings. - No feature flag: the change only adds a fallback where the cell was previously blank, and cannot regress a row that already rendered.
- No database or migration changes.

