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.
- CreditUsage ships two answers to the same chargeback question, and the choice between them is a choice about where sensitive tenant data is allowed to live.
- The browser app is the trust boundary: 161 KB of vanilla JavaScript that parses and joins tenant data with no server, no framework, and no network call.
- The join on user principal name is the whole ballgame, and the demo data is built to expose what breaks when you key on display name instead.
- The demo is the test fixture: synthetic CSVs run through the same production parse and join pipeline as real exports.
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.
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.
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.
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 Import | Power BI: Direct Query | Browser App | |
|---|---|---|---|
| Data path | Admin center + Entra exports | Viva Insights Consumption query | Two CSVs, parsed in-browser |
| Refresh | Manual, monthly | Scheduled | Manual, on demand |
| Trust boundary | Power BI Service tenant | Power BI Service tenant | The analyst's machine |
| Sharing | Workspace, row-level security | Workspace, row-level security | File downloads only |
| Setup cost | Low | Viva Insights access required | Lowest (open a URL) |
| Audit and history | Yes | Yes | No |
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.
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.
cache.addAllis atomic and its failure is swallowed. One missing file means no precache entries are stored, silently.- The demo credits CSV carries an
Agents and Servicescolumn (values likeCowork; Work IQ; Researcher; Analyst) that is not inCOLUMN_CANDIDATESand contradicts the README's "single credit stream" claim. Worth asking the maintainers. - Short aliases like
credits,user, andcccan misread an unrelated column. - No
package.json, no test configuration, no build step.
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.





