You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Finance asks "what did the support agent cost last month versus the HR bot?"
Both share one client_id, so without tags the answer is "we don't know." With
tags it is a group-by.
Acceptance criteria
The key set is fixed — team, project, env, enduser.id. An
unknown key is a config error, not a silently-dropped header.
Values are length-limited and validated so nobody stuffs JSON into a header.
Tags appear both as request headers and as donkey.cost.* span attributes.
Until the gateway-side header names are verified, tags still carry full
value in spans, which the SDK controls end to end.
Cross-surface impact
core/telemetry: the change.
Verification: the attribution header names are the single most important
unverified value; keep the Unverified(...) placeholder and config override.
Implementation plan (branch feat/196-cost-attribution-tags)
Scope decision: config-level "set once" plus per-run overrides via donkey.run(...) — fulfilling the two docstrings (Donkey.run / RunScope)
that already promised cost fields land on donkey.run() in #196, alongside the
AC's Donkey.from_env(team=..., project=..., env=...) set-once surface.
The fixed key set
team, project, env, enduser.id. A new frozen CostTags dataclass
(core/cost.py) with fields team/project/env/enduser_id. CostTags.from_mapping
(toml path) rejects any key outside the fixed set → ConfigError (AC #1).
Values are length-limited (≤256) and reject control chars / CR / LF so nothing
can smuggle JSON or inject a header (AC #2).
Two carriers (AC #3, #4)
Request headers — emitted with UNVERIFIED placeholder names (§0.3):
four new Unverified(...) constants in core/_verify.py (X-Anypoint-Cost-*
best-guess), each config-overridable (cost_team_header …) and resolved once
per client like the correlation headers (telemetry: per-call and per-run correlation IDs via donkey.run() #195). §3 in docs/verified-apis.md
confirms the cost-attribution header name is the single highest-priority
unknown.
donkey.cost.* span attributes — always carry the full value (SDK
controls the span end to end), so attribution works even before the header
name is verified. donkey.cost.team exists; add .project / .env / .enduser.id.
Resolution / precedence
DonkeyConfig.cost: CostTags, resolved in from_env() along the fixed
precedence (kwarg → DONKEY_COST_* env → [donkey.cost] toml → default).
Donkey.from_env(*, team=, project=, env=, enduser_id=) builds the override.
donkey.run(id=, team=, project=, env=, enduser_id=) binds a CostTags to a
contextvar for the block; per-send injection and span record merge run-scope over config (per field). The static connection_kwargs()
snapshot carries config-level tags only — run-scope can't propagate through a
frozen default_headers dict (documented degradation, same as correlation).
Surfaces moving in lockstep
core/cost.py (new), core/config.py, core/_verify.py, core/telemetry.py, core/transport.py (async + sync twin), donkey.py; tests under tests/unit; docs/verified-apis.md §3 rows; website telemetry/config page + README. The
Verification worklist gains the four cost-header-name rows (blocked-on-verification).
Activity
tommasobolis commentedon Sep 11, 2026
Implementation plan (branch
feat/196-cost-attribution-tags)Scope decision: config-level "set once" plus per-run overrides via
donkey.run(...)— fulfilling the two docstrings (Donkey.run/RunScope)that already promised cost fields land on
donkey.run()in #196, alongside theAC's
Donkey.from_env(team=..., project=..., env=...)set-once surface.The fixed key set
team,project,env,enduser.id. A new frozenCostTagsdataclass(
core/cost.py) with fieldsteam/project/env/enduser_id.CostTags.from_mapping(toml path) rejects any key outside the fixed set →
ConfigError(AC #1).Values are length-limited (≤256) and reject control chars / CR / LF so nothing
can smuggle JSON or inject a header (AC #2).
Two carriers (AC #3, #4)
four new
Unverified(...)constants incore/_verify.py(X-Anypoint-Cost-*best-guess), each config-overridable (
cost_team_header…) and resolved onceper client like the correlation headers (telemetry: per-call and per-run correlation IDs via donkey.run() #195). §3 in
docs/verified-apis.mdconfirms the cost-attribution header name is the single highest-priority
unknown.
donkey.cost.*span attributes — always carry the full value (SDKcontrols the span end to end), so attribution works even before the header
name is verified.
donkey.cost.teamexists; add.project/.env/.enduser.id.Resolution / precedence
DonkeyConfig.cost: CostTags, resolved infrom_env()along the fixedprecedence (kwarg →
DONKEY_COST_*env →[donkey.cost]toml → default).Donkey.from_env(*, team=, project=, env=, enduser_id=)builds the override.donkey.run(id=, team=, project=, env=, enduser_id=)binds aCostTagsto acontextvar for the block; per-send injection and span record merge
run-scope over config (per field). The static
connection_kwargs()snapshot carries config-level tags only — run-scope can't propagate through a
frozen
default_headersdict (documented degradation, same as correlation).Surfaces moving in lockstep
core/cost.py(new),core/config.py,core/_verify.py,core/telemetry.py,core/transport.py(async + sync twin),donkey.py; tests undertests/unit;docs/verified-apis.md§3 rows; website telemetry/config page + README. TheVerification worklist gains the four cost-header-name rows (blocked-on-verification).
feat(telemetry): validated cost-attribution tags — config + run() ove…
tommasobolis commentedon Sep 11, 2026
Landed on
developvia #360 (squash-merged). Cost-attribution tags shipped: config-level (Donkey.from_env(team=…),DONKEY_COST_*,[donkey.cost]) + per-run overrides (donkey.run(team=…)), emitted as request headers anddonkey.cost.*span attributes. Closes the six-piece minimum.