Skip to content

fix(attribution): priority meant the opposite of what it said - #118

Open
getnable wants to merge 1 commit into
mainfrom
fix/tag-attribution
Open

fix(attribution): priority meant the opposite of what it said#118
getnable wants to merge 1 commit into
mainfrom
fix/tag-attribution

Conversation

@getnable

Copy link
Copy Markdown
Owner

Three things in the path that decides which team owns which dollar.

1. priority meant the opposite of what it said

Documented as "lower number = higher priority". Implemented as rules sorted ascending with every match overwriting the last, so the highest number won. Both examples in the file write_example_rules() generates for users were wrong:

{team: frontend, costcenter: CC-9910}  ->  CC-9910     should be frontend
{team: infra-core}                     ->  infra-core  should be platform

The first is the expensive one. An org that tags properly got teams named after cost centre codes. Every dollar still lands in the correct total, so this doesn't look like a broken report; it looks like a team disputing an invoice months later.

Now first-matching-rule-wins per field. Fields still resolve independently, and fallbacks still fire when the higher-priority tag is absent, which is what an over-correction would break.

This was already known. test_mapper.py carried a characterization test pinning the buggy behaviour, with a comment saying the disagreement was flagged and deliberately left alone because a perf-only refactor was the wrong place to shift anyone's numbers. Right call then. This is the right place, so that test now encodes the corrected contract.

⚠️ This changes attribution for anyone whose rules had a field collision. That is the point, and it needs a release note, not a footnote.

2. Empty attribution now explains itself

In AWS a tag does not reach Cost Explorer or the CUR until it's activated as a cost allocation tag in Billing. Separate step, different console. Nothing errors when it's missing: every dollar lands in unattributed, and a user who tagged their whole estate correctly concludes we're broken.

Two rules it obeys:

It never reports "inactive" when the check itself failed. A guess about someone's billing configuration is worse than silence, because they'll act on it.

It goes through billing_access.ce_client like every other Cost Explorer call rather than restating half the policy, so it's refused in demo mode, in scheduled work, and under NABLE_NO_COST_EXPLORER=1. Cost Explorer bills per request, and "surely one more call for a diagnostic" is how a per-request charge ends up on a timer. The ratchet in test_billing_access.py would have caught a direct client here, and did.

3. docs/COST-ATTRIBUTION.md, because there was nothing

Prior documentation: one line in everything-nable.md and a module docstring. An enterprise evaluator asking how spend maps to teams had nothing to read.

Covers what it needs from you, that Terraform is optional and only for writing tags back, the AWS activation step, Azure's inheritance policy, GCP labels, and a plainly worded list of what it cannot do yet: tag-only matching, no shared-cost allocation, three fixed dimensions, no account hierarchy. Those limits are the honest answer to a buyer comparing tools.

Mutation-tested, 7 of 7 caught. Suite 2739 passing.

🤖 Generated with Claude Code

…pty results said nothing

Three things, all in the path that decides which team owns which dollar.

1. `priority` was inverted.

Documented as "lower number = higher priority". Implemented as rules sorted
ascending with every match overwriting the last, so the HIGHEST number won. Both
examples in the file write_example_rules() generates for users were wrong:

  {team: frontend, costcenter: CC-9910} -> CC-9910   should be frontend
  {team: infra-core}                    -> infra-core should be platform

The first is the expensive one. An org that tags properly got teams named after
cost centre codes, and since every dollar still lands in the correct total, the
symptom is not a broken report. It is a team disputing an invoice months later.

Now first-matching-rule-wins per field, which is what the docs describe and what
priority means everywhere else. Fields still resolve independently, and fallbacks
still fire when the higher-priority tag is absent, which is the thing an
over-correction would break.

This was already known. tests/attribution/test_mapper.py carried a
characterisation test pinning the buggy behaviour, with a comment saying the
disagreement was flagged and left alone because a perf-only refactor was the
wrong place to shift anyone's numbers. That was the right call then. This is the
right place, so the test now encodes the corrected contract.

NOTE: this changes attribution for anyone whose rules had a field collision.
That is the point, and it belongs in the release notes rather than a footnote.

2. Empty attribution now explains itself.

In AWS a tag does not reach Cost Explorer or the CUR until it is activated as a
cost allocation tag in Billing, which is a separate step in a different console.
Nothing errors when it is missing: every dollar simply lands in `unattributed`,
and a user who tagged their whole estate correctly concludes we are broken.

When a report comes back with nothing attributed, nable now says which configured
keys AWS is not carrying, and that activation is not retroactive.

Two rules it obeys. It never reports "inactive" when the check itself failed: a
guess about someone's billing configuration is worse than silence, because they
will act on it. And it goes through billing_access.ce_client like every other
Cost Explorer call rather than restating half the policy, so it is refused in
demo mode, in scheduled work, and under NABLE_NO_COST_EXPLORER=1. Cost Explorer
bills per request, and "surely one more call for a diagnostic" is exactly how a
per-request charge ends up on a timer. The ratchet in test_billing_access.py
would have caught a direct client here, and did.

3. docs/COST-ATTRIBUTION.md, because there was nothing.

Total prior documentation for this feature was one line in everything-nable.md
and a module docstring. An enterprise evaluator asking how spend maps to teams,
which is question two or three in any FinOps evaluation, had nothing to read.

Covers what it needs from you, that Terraform is optional and only for writing
tags back, the AWS activation step, the Azure inheritance policy, GCP labels, and
a plainly worded list of what it cannot do yet: tag-only matching, no shared cost
allocation, three fixed dimensions, no account hierarchy. Those limits are the
honest answer to a buyer comparing tools, and hiding them wastes everyone's time.

Mutation-tested, 7 of 7 caught. Suite 2739 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant