Credits Dashboard loses project attribution after project deletion

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Bug Summary

The GitLab Credits user event table resolves an event's location from its current project_id. When the referenced project has been deleted, the lookup returns no project and the Location cell is blank, even though the historical billing event still contains the project ID.

This makes a valid credit charge appear unattributable and prevents group owners or instance administrators from understanding where the credits were consumed.

This is different from #591437: in this case, project_id is present on the billing event, but the referenced project no longer exists.

Steps to reproduce

  1. Generate a billable GitLab Duo or Duo Agent Platform event in a project.
  2. Verify that the event appears in GitLab Credits > Usage by user > User details with the project in the Location column.
  3. Delete the project and allow the deletion to complete.
  4. Return to the same historical usage event.
  5. Observe that the Location cell is empty.

What is the current bug behavior?

The event contains a project_id, but GitLab resolves the location by loading the current Project record. After deletion, the resolver returns null, and the frontend renders an empty Location cell.

The current path is:

  • CustomersDot returns projectId with each usage event.
  • Types::GitlabSubscriptions::SubscriptionUsage::EventType#location loads Project by that ID.
  • The usage events table only renders Location when the resulting object exists.

The same lookup is also used when constructing DAP session links, so deletion removes both the visible attribution and any project-based session URL.

What is the expected correct behavior?

Historical credit usage must remain understandable after its originating project is deleted.

At minimum, when a billing event has a project_id but the project cannot be resolved, the dashboard should show a non-linked fallback such as:

Deleted or unavailable project (ID: 12345)

If usage billing stores an immutable project path snapshot, the dashboard could instead show:

Deleted or unavailable project (previous path: group/project)

The implementation must not reveal a current private project to a viewer who lacks access. Product and security should decide whether deleted and inaccessible projects use the same user-facing state.

Legacy events for which no path snapshot exists should continue to work and fall back to the project ID.

Reproducibility

Expected to occur consistently for any historical usage event whose non-null project_id references a project that has completed deletion.

Impact Assessment

  • Customers see credits consumed without enough context to reconcile the charge.
  • Billing transparency and trust are reduced.
  • Support must use internal billing data to explain otherwise valid usage.
  • Historical CSV/API consumers may have the same attribution gap if they also resolve project metadata at read time.

Severity

Suggested severity: severity::3. Credit calculation is not affected, but customer-visible billing attribution is lost.

Environment

Observed on GitLab.com.

  • #579139 (closed) introduced the project/session association used by Credits Dashboard rows.
  • #591437 tracks billing events where project_id is missing at emission time. It is related but does not cover project deletion after an event with a valid project ID has been recorded.

Possible fixes

Iteration 1: resilient fallback

Preserve the raw project ID on the GraphQL event and expose whether a non-null project reference could be resolved. Render a non-linked deleted/unavailable fallback in the event table instead of an empty cell.

This is the smallest change and works for existing historical rows.

Iteration 2: immutable attribution snapshot

Capture a minimal project identity snapshot, such as the full path at event ingestion time, and propagate it through Usage Billing, CustomersDot, and the GitLab GraphQL API. Use the snapshot only when the live project cannot be resolved.

This needs decisions about retention, project renames/transfers, privacy, and whether paths may be exposed after deletion. Retaining an entire deleted Project record solely for billing attribution is not required.

Acceptance criteria

  • A usage event with a non-null project_id does not render a blank Location solely because the project was deleted.
  • The fallback is not linked to a nonexistent project.
  • Legacy events without a stored project path remain valid.
  • The UI does not disclose current private-project metadata to unauthorized viewers.
  • Tests cover an existing project, a deleted/missing project, a genuinely project-less event, and an inaccessible project.
Edited by 🤖 GitLab Bot 🤖