[UI] Add the billable usage export review page
What does this MR do and why?
Adds a read-only admin page that previews the air-gapped billable usage export before the admin downloads it.
It lives at Admin > Subscription > "Export usage report".
An air-gapped self-managed instance can't send usage data to the Customers Portal over the network, so an admin downloads a JSON report and uploads it by hand instead. Until now there was no way to see what the report contained before downloading it.
The page shows the report period, when the term was last exported, a warning naming any days with no recorded data, per-product capability consumption panels, a seats panel, and Export JSON / Cancel actions, all covered in the screenshots below. Exporting downloads the file and returns the admin to the subscription page with a success alert.
Each report covers one subscription term. The page first loads the exportable terms from the terms endpoint, then the report for the current term. When there is a previous term (after a renewal), a "Subscription term" dropdown lets the admin switch to it. Picking a term reloads the report, and the seats panel and the downloaded file follow the selected term.
Notable design decisions
- Meter display names mirror CustomersDot's
Billing::Usage::Product::PRODUCT_LISTandBilling::Usage::FlowTypes::FLOW_TYPE_LIST, which decide how uploaded usage is named and grouped. The frontend catalog is keyed on the workflow, the first/-delimited segment ofmetadata.feature_qualified_name- similar to CustomersDot's own fixtures likesecrets_read/v1andcode_review/v1. - The days-with-no-data warning is derived only from seat link coverage. Seat link is written daily by a cron regardless of activity, so a missing day proves the instance recorded nothing. A billable usage row only exists once an event happens, so a missing day there is indistinguishable from a quiet day, and
CoverageServicedeliberately reports only a count for it. - Two small deviations from the prototype. The last export is tracked per term, so a term that was never exported says "No report has been exported for this term yet." instead of "from this instance yet". The last-exported time shows in the viewer's timezone (for example "Aug 1, 2026, 9:12 AM GMT"), where the design shows UTC.
- On Admin > Subscription, Export usage report replaces the Export license usage file button whenever the report is available (the
billable_usage_exportflag on and an offline licence). This follows the design issue ("The Export license usage file button on Admin › Subscription becomes Export usage report for offline licences on 19.6 or later"). The JSON carries the same daily seat link series as the CSV, and CustomersDot already accepts it on upload for both Flex and non-Flex subscriptions. The CSV export itself stays:/admin/license/usage_export.csvis unchanged, and the monthly reminder banner still links to it until the banner is updated in a follow-up. One gap remains: CustomersDot's email route (OfflineProvisioningMailbox) only reads the CSV, so admins who email the file rather than upload it still need the CSV.
Reviewer notes:
- Targets the branch
18552-export-endpoint, notmaster. Stacked on the export payload, terms and download endpoints in !257491 (merged). - Behind the
billable_usage_exportfeature flag,default_enabled: false.
Known gaps, deliberately not in this MR:
Export CSV for reviewbutton is removed - since exported data is collapsed without root_namespace_id - an explicit preview is not needed anymore.- No Duo Agent Platform data yet: DAP emits from the AI Gateway and no monolith endpoint receives it, tracked in https://gitlab.com/groups/gitlab-org/-/work_items/23209; the page renders Secrets Manager meters only until it lands. (Screenshot above show DAP based on local seeded data)
References
- Closes https://gitlab.com/gitlab-org/customers-gitlab-com/-/work_items/18553
- Export payload and endpoint issue: https://gitlab.com/gitlab-org/customers-gitlab-com/-/work_items/18552
- Design and UX issue: https://gitlab.com/gitlab-org/fulfillment/designers/usage-billing-explorations/flex/usage-billing-flex-air-gapped-v2/-/work_items/1
- Interactive prototype: https://gitlab-org.gitlab.io/fulfillment/designers/usage-billing-explorations/flex/usage-billing-flex-air-gapped-v2/
Screenshots or screen recordings
How to set up and validate locally
- Check out this branch (it's based on
18552-export-endpoint). - In the Rails console, enable the flags:
Feature.enable(:billable_usage_export)andFeature.enable(:local_billing_persistence). - An offline cloud licence is required, because
local_persistence_enabled?requiresLicense.currentto not be an online cloud licence. Note that this can't be minted on a local GDK, since only the public key is present. I worked around it locally by temporarily stubbing that method, which is not part of this MR. - Seed some data: create
Utilization::BillableUsage::DailyAggregaterecords with the:billable_usage_daily_aggregatefactory across a date range, plus someHistoricalDatarows. Skip a couple of days ofHistoricalDataso the no-data warning renders. - Go to Admin > Subscription and click "Export usage report".
- Check the period, the capability panels, the seats panel and the no-data warning, then click "Export JSON" and confirm the file downloads and the subscription page shows the success alert.
- To see the "Subscription term" dropdown, add a second, expired offline licence to the licence history. Without a previous term the dropdown is hidden. Switch terms and check that the dates, licensed seats and the download filename change.
- Run the frontend tests:
node_modules/.bin/jest --config jest.config.js ee/spec/frontend/admin/subscriptions/
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.




