fix(selfhost): stop the redaction scrubber nulling every AI token count - #10212
Conversation
SECRET_KEY matches /token/i, so scrubRecord -- wired as posthog-node's before_send -- rewrote PostHog's own $ai_input_tokens and $ai_output_tokens to the "[redacted]" STRING, which PostHog then coerced to null on its numerically-typed properties. The result: not one AI call in the project has ever carried a token count. posthog.ai_events.input_tokens/output_tokens/total_tokens are NULL for every model over the retention window, including claude-sonnet-5 at 2,321 calls and $498.43 of real spend. $ai_total_cost_usd came through untouched because it has no secret-shaped word in it. PostHog derives $ai_input_cost_usd/$ai_output_cost_usd from tokens, so those could not be computed either -- and the miner-side split landed in #10199 would have been scrubbed the same way. A secret-shaped key holding a NUMBER is a counter, not a credential: every secret this module exists to catch is a string, and there is no numeric form of one to leak. Skip redaction for numbers only; a string, object, array or boolean under the same key is still redacted exactly as before. Deliberately general rather than an allowlist of the two $ai_* keys -- an allowlist goes stale the moment PostHog adds $ai_cache_read_input_tokens, and it would fail the same silent way. Closes #10211
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-31 13:02:38 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10212 +/- ##
==========================================
- Coverage 92.21% 91.34% -0.87%
==========================================
Files 935 935
Lines 114263 114263
Branches 27620 27620
==========================================
- Hits 105371 104377 -994
- Misses 7588 8777 +1189
+ Partials 1304 1109 -195
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Summary
Not one AI call in the project has ever carried a token count. The privacy scrubber was eating them.
SECRET_KEYmatches/token/i, andscrubRecord— wired as posthog-node'sbefore_send— redacts on key match. So PostHog's own$ai_input_tokensand$ai_output_tokenswere rewritten to the"[redacted]"string, which PostHog then coerced tonullon its numerically-typed properties. Confirmed on a real live event:{"$ai_input_tokens":null,"$ai_output_tokens":null,"$ai_latency":10.308, "$ai_model":"claude-sonnet-5","$ai_provider":"claude-code", "$ai_total_cost_usd":0.44771210000000006, ...}$ai_total_cost_usdsurvives because it has no secret-shaped word in it. The token fields do not.Impact
posthog.ai_events.input_tokens/output_tokens/total_tokensare NULL for every model over the retention window — includingclaude-sonnet-5at 2,321 calls and $498.43 of real spend. PostHog's LLM cost views break spend down by token usage, so the ORB shows dollars but no tokens/call, no input-vs-output ratio, and no cost-per-token.$ai_input_cost_usd/$ai_output_cost_usd, which PostHog derives from tokens, could not be computed at all.It would also have silently defeated the miner-side token split landed in #10199: those events pass through the same scrubber.
Fix
A secret-shaped key holding a number is a counter, not a credential. Every secret this module exists to catch — a token, an API key, a password, a cookie, a DSN, a bearer header — is a string; there is no numeric form of one to leak. Numbers are skipped; a string, object, array or boolean under the same key is still redacted exactly as before.
Deliberately general rather than an allowlist of the two
$ai_*keys. An allowlist goes stale the moment PostHog adds$ai_cache_read_input_tokensor the code adds another counter — and it would fail the same silent way this bug did.Closes #10211
Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Closes #123) — a linked open issue is required for every contributor PR.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally;codecov/patchrequires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateIf any required check was skipped, explain why:
selfhost-redaction-scrub,selfhost-posthog,redaction,prompt-packet-redaction(264 tests). Coverage measured scoped to the changed file: 100% of the changed lines and branches, verified line-by-line against lcov rather than read off a summary.Safety
UI Evidencesection below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.This narrows a redaction rule, so it deserves the scrutiny. The carve-out is numbers only, and three of the four new tests exist specifically to pin the boundary: a string under
api_token/password/authorization/session_cookieis still redacted, and so is an object, an array, or a boolean under a secret-shaped key. The number case cannot carry a credential — a bearer token, PAT, JWT, DSN, cookie or password has no numeric representation. Value-based scrubbing (SECRET_VALUE, the JWT and query-string paths) is untouched and still applies to every string regardless of its key.UI Evidence
Not applicable — no visible UI, frontend, docs, or extension change.
Notes
Found while investigating why PostHog's LLM Analytics Token usage column was empty for every trace. The same investigation confirmed the ORB is emitting the events correctly — cost, latency, model, provider, repo and trace id all arrive intact — so this was the only thing standing between the project and real token metrics.