fix(hooks): classify company-credential Claude sessions as team#4259
Merged
daviddanialy merged 2 commits intoJul 16, 2026
Conversation
Claude sessions authenticated by company credentials (API key, gateway/ proxy, Bedrock, Vertex) emit no user.account_uuid, so account attribution no-op'd and their entire spend fell into the "(unset)" account-type bucket on the Costs page. A personal Claude subscription always signs in via OAuth and always emits the UUID, so its absence means company credentials: classify these sessions team. Attribution now always classifies and stamps account_type; the user_accounts entity, device bridge, and billing mode — all keyed on the absent UUID — are still skipped, and the OTEL fast path keys on the resolved AccountType for these sessions (a UUID-bearing session still requires a persisted UserAccountID so transiently failed entity persistence retries on the next batch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 50228f6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
The device bridge keys on the per-device id, not the account UUID, so a UUID-less gateway session with a resolved work email must still teach device -> employee: at a gateway-only org it is the only session shape that ever can, and without it a personal account later seen on the same device could never be attributed to its employee. Only the user_accounts entity and billing mode key on the UUID and remain skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Auto-approved: review:bypass label applied by @daviddanialy. Required status checks still gate this merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On the Costs page's account-type breakdown, orgs whose engineers run Claude Code against company credentials (API key, gateway/proxy, Bedrock, Vertex) see nearly all spend under "(unset)" — e.g. SmartNews: 98.3% / $63k unset, with 273 work-email users unclassified.
Root cause: only a personal Claude subscription (OAuth login) emits
user.account_uuid; company-credential auth never does.attributeSessionearly-returned on the empty UUID before classifying, soaccount_typewas never stamped.Verified in prod (30d, session-level, native
claude-codeOTEL):Same client versions show 0–5% UUID rows at SmartNews vs 99.4–100% at MoonPay, ruling out a client-version artifact — the discriminator is auth mode.
Fix
Absence of
user.account_uuidmeans no personal account is behind the session, so it is company credentials — classify team:classifyAccount: emptyExternalAccountUUID→team(with a documented residual gap for first-batch UUID timing and pre-UUID clients).attributeSession: always classifies and stampsAccountType; theuser_accountsentity, device bridge, and billing mode — all keyed on the absent,NOT NULLUUID — are still skipped.AccountTypefor UUID-less sessions (they never get aUserAccountID), while UUID-bearing sessions still require a persistedUserAccountIDso a transiently failed entity upsert retries on the next batch.sessionEnrichesAttribution: a late-arrivinguser.account_uuid(mid-session OAuth login) re-attributes and persists the real account entity.No Postgres row is created for these sessions (
user_accounts.external_account_uuidis the entity key); the stamped ClickHouseaccount_typeis what the cost surfaces consume.Scope
Applies at ingest (not retroactive) and covers Claude OTEL-emitting sessions. Codex/Cursor set
account_type=""and never run attribution — their spend stays unset (separate feature).Testing
TestLogs_ClassifiesCompanyCredentialSessionAsTeam: UUID-less session →teamstamped on telemetry, nouser_accountsrow.mise build:server+mise lint:serverclean.🤖 Generated with Claude Code
Summary by cubic
Classifies Claude sessions using company credentials (API key, gateway/proxy, Bedrock, Vertex) as team when
user.account_uuidis absent, preventing spend from landing in the "(unset)" bucket on the Costs page. Attribution now always stampsaccount_typeand, for UUID-less sessions, participates in the device bridge while skipping UUID-keyed persistence.classifyAccount: emptyExternalAccountUUID→team.attributeSession: always stampsAccountType; teaches/resolves the device bridge byDeviceIDeven without a UUID; if UUID is missing, skipuser_accountsupsert and billing mode.AccountType; UUID-bearing sessions still require a persistedUserAccountIDto retry on failure.user.account_uuidre-attributes and persists the correct account mid-session.Written for commit 50228f6. Summary will update on new commits.