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_id nor a namespace_id keep rendering an empty cell — there is no attribution to show. That case is tracked in #591437.

References

#608133

Screenshots or screen recordings

Before After
image image

How to set up and validate locally

  1. yarn storybook:start
  2. Open the ee/usage_quotas/usage_billing/users/show/app → Default story.
  3. 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.pot regenerated 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.

Merge request reports

Loading
Loading