CreditUsage Puts Copilot Chargebacks in the Browser

Microsoft's FinOps tool ships a Power BI report and a zero-framework web app that answer the same question. The choice between them is really a choice about where sensitive tenant data is allowed to live.

9 min read • View on GitHub • More from microsoft

A wide editorial illustration of a wooden analyst's desk with an open laptop at center. Its screen shows a small hand-drawn dashboard of a bar chart, a pie, and a table of names. Two large sheets of paper covered in rows of numbers slide into a slot at the top edge of the screen, feeding the dashboard. A heavy inked boundary line runs around the entire laptop like a fence, and outside the fence a telephone pole holds a severed network cable while a small cloud is crossed out with a single line. The scene explains the tool's trust boundary: tenant data flows into the dashboard but never crosses the fence.
The browser is the trust boundary. Data flows into the dashboard, but nothing crosses the fence.
Key Takeaways

One Question, Two Trust Postures

A Copilot credit bill lands. It is a single number, and it is not small. Someone in finance has to split it across departments, and the export in hand lists per-user consumption, not org structure. The join between those two facts is the whole problem.

Microsoft's CreditUsage answers that problem twice, in one repository. First: a Power BI report, delivered as .pbix and .pbit templates, living inside the Power BI Service tenant with row-level security and scheduled refresh. Second: a browser app that parses two CSVs locally, renders ten tabs of dashboards, and never sends a byte anywhere.

Same question. Two trust postures. The interesting decision is not the feature list. It is where the data is allowed to live.

Why Copilot Credits Need a Chargeback Tool

Copilot consumption bills at $0.01 per credit. The Microsoft 365 admin center exports per-user consumption. Entra holds the org chart: department, business unit, cost center, manager. CreditUsage joins the two on the user principal name, then attributes spend to departments and flags capacity risk.

The repository is MIT-licensed, built by Microsoft, and distributed as Power BI templates plus the web app. Its own description frames the job plainly.

Monitor how Copilot credits are consumed, attribute usage to departments and surface capacity risk across your organization.

microsoft/CreditUsage maintainers, Project description · microsoft/CreditUsage

The scope is a finance workflow, not a monitoring dashboard: PayGo chargebacks, allocation of a prepaid pool by credits used, headcount, or budgeted allowances, prepaid coverage and overage, high-consumption individuals, and monthly credit projections against capacity.

A tight close-up editorial illustration of a row of wicker baskets on a wooden table, each basket tagged with a department name. Dozens of small paper name tags with tiny credit figures flutter down from above, each landing in exactly one basket. A hand holding a rubber stamp presses a total onto the lid of the nearest basket, while one tag hovers unresolved between two baskets. The scene depicts the tool's core job, turning per-user credit consumption into departmental invoices, and foreshadows the join failure that a display-name key causes.
Per-user consumption becomes departmental invoices. The hovering tag is what happens when the join key is not unique.

How the Browser App Works

Drop two CSVs in either order. The app holds them in state.pending, waits until both are present, then runs one pipeline: parse, normalize, join on UPN, compute, render.

The CSV parser is hand-written, character by character. It handles quoted commas, escaped double quotes, embedded newlines inside quoted fields, CRLF endings, and a byte-order mark. That matters because real exports contain real names, and real names contain commas. Pulling in a CSV library would have been easier and would have added a dependency to vet. The file's header comment states the choice directly: "No frameworks, no network calls."

Column headers resolve through a candidate list, so Monthly credits used, credits used, creditsused, and cowork credits all land in the same field. The DIMS array maps display labels to normalized fields, which lets one aggregation function serve every slicer. Adding a dimension is one line, provided the alias list is updated too.

The join is where the tool lives or dies. Key it wrong and the output is wrong, with nothing warning you.

The join is invisible in a screenshot and decisive in practice. Switch the key from UPN to display name and watch the invoices go wrong.

When an export has no credit limit column, the app assigns a fallback of 400 credits and sets a flag so limit-based metrics can be shown as estimates. That is good handling of incomplete data, though the README slightly understates it by saying the limit "comes from this export."

The Demo Is the Test Fixture

The synthetic demo data is not a separate mock. It is embedded as template literals and flows through the same parseCSV and join pipeline as a real export. The demo exercises the production code path. The file says, in its own comment, "Not for real decisions."

That small decision has a large consequence: the demo doubles as a test fixture for the join logic. And the demo is built to fail the naive approach. The Entra sample contains two different users both named "Jordan Nguyen," sharing a cost center. A join keyed on display name merges them into one invoice line. The demo data exists to expose exactly that failure mode.

Three Ways to Answer, One Decision to Make

The repository offers three paths to the same number, and they trade freshness against trust.

Power BI: CSV ImportPower BI: Direct QueryBrowser App
Data pathAdmin center + Entra exportsViva Insights Consumption queryTwo CSVs, parsed in-browser
RefreshManual, monthlyScheduledManual, on demand
Trust boundaryPower BI Service tenantPower BI Service tenantThe analyst's machine
SharingWorkspace, row-level securityWorkspace, row-level securityFile downloads only
Setup costLowViva Insights access requiredLowest (open a URL)
Audit and historyYesYesNo

The browser app has the lowest setup cost and the weakest audit trail. Direct Query is the freshest and needs Viva Insights Advanced Insights access. CSV import is the middle ground most teams will start with.

A product-pattern precursor is worth noting. Microsoft's own Azure Usage and Billing Portal, open sourced years earlier, also surfaced usage and billing data through Power BI.

Today we're happy to announce that the Azure Usage and Billing Portal has been open sourced and is now available on GitHub.

Microsoft Azure, Official announcement · Azure Usage and Billing Portal

The two tools are not the same. The Azure portal was a deployed, multi-component ingestion system for Azure resource billing. The available sources do not establish that CreditUsage was derived from it. Treat it as a shared pattern, not a lineage.

Where the Edges Are Rough

The service worker reads as deliberate and has not been exercised carefully. It is cache-first, with a hand-edited cache name, so every release depends on someone remembering to bump it.

var CACHE_NAME = 'cowork-credits-v1.1-1';

self.addEventListener('fetch', function (event) {
    if (event.request.method !== 'GET') return;
    event.respondWith(
        caches.match(event.request).then(function (cached) {
            if (cached) return cached;
            return fetch(event.request)
                .then(function (response) { /* cache and return */ })
                .catch(function () { return cached; }); // cached is undefined here
        })
    );
});

The catch branch returns cached, but the code only reaches the network path when there is no cache entry. That value is always undefined, so an offline request for an uncached resource returns undefined to respondWith and produces a network error. The intent was a fallback. The wiring is wrong.

None of this makes the tool wrong. It makes it young. The pipeline is sound, the escaping is consistent, and the demo data is honest about its own limits. The rough edges are in the offline layer and the long tail of header conventions, which is exactly where a tool that trusts arbitrary exports should expect trouble.