Repository navigation
fix(node): honor privacyMode set as a client option - #5296
posthog[bot] wants to merge 1 commit into
Conversation
PostHogOptions declared `privacyMode`, but the Node client never read, stored or exposed it, so `@posthog/ai`'s `withPrivacyMode` — which reads the value off the client — always saw `undefined`. Only the per-call `privacyMode` argument worked. Store the normalized option as `public readonly privacyMode` alongside `enableFullAiCapture`, and let `withPrivacyMode` accept either that name or the historical `privacy_mode` spelling callers set directly on a client. Brings posthog-node in line with the capture-ai spec's "privacy mode always wins" requirement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: cd2e9a45-38e5-4bda-b490-640185728783
Coherence2 spec problems · 0 failing chokepoints · 2 spec gaps · 0 guard failures 44 components, 5 bullets (5 invariants, 0 requirements), 2 problems Spec problems (spec --check)
Chokepoints: none declared. Spec gaps: 2 entrances with outside or unknown trust and no traced control, and 10 more held by the adoption baseline
Close one with |
Node SDK compliance v2Commit: 6810c1f Capture v0 — acceptance✅ 5 passed / ❌ 0 non-passing / Drill-down: run artifacts → Harness exit: 0; report validation exit: 0. All selected cases passed and report validation succeeded. Capture v1 — acceptance✅ 5 passed / ❌ 0 non-passing / Drill-down: run artifacts → Harness exit: 0; report validation exit: 0. All selected cases passed and report validation succeeded. Capture v0 — migration✅ 55 passed / ❌ 1 non-passing / Drill-down: run artifacts → ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. Capture v1 — migration✅ 120 passed / ❌ 1 non-passing / Drill-down: run artifacts → ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. |
Problem
PostHogOptions.privacyModeis documented as "privacy mode always wins", but the Node client never read, stored or exposed it.@posthog/ai'swithPrivacyModeresolves the client-level setting by reading it off the client instance, so for a realnew PostHog(key, { privacyMode: true })client it always sawundefinedand redacted nothing. Only the per-callprivacyModeargument worked. The existing@posthog/aitests covered this path by monkey-patchingprivacy_modeonto a mock client — something a real client could never reproduce.This brings the Capture AI contract into compliance with
openspec/specs/capture-ai/spec.md, which requires thatenableFullAiCapturebe overridden by privacy mode "regardless of client-level or per-call configuration", with the scenario "privacy mode wins over the flag" — given the client has privacy mode enabled and the flag set to true, the captured event should carry no input or output content.Compliance matrix note (
compliance/posthog-node.md, n6 — Capture AI, 🟡 Partial):Changes
packages/node/src/client.ts— store the normalized option aspublic readonly privacyMode, mirroring howenableFullAiCaptureis already resolved in the constructor.packages/node/src/types.ts— document theprivacyModeoption (it previously had no doc comment).packages/ai/src/utils.ts—withPrivacyModenow redacts on eitherprivacyMode(thePostHogOptionsname the client now carries) or the historicalprivacy_modespelling callers set directly on a client.This also fixes
packages/ai/src/claude-agent-sdk/processor.ts, which already readposthog?.privacyModeoff the client and so was subject to the same dead option.Backwards compatibility
Backwards-compatible, and strictly strengthening. No caller gets any redaction benefit from the client-level
privacyModeoption today, because it does nothing — wiring it up can only add redaction for callers who already believed they had set it. The legacyprivacy_modespelling keeps working, no public signature changes, and nothing is removed.Testing performed
packages/node/src/__tests__/ai-capture/wiring.spec.ts:privacyModedefaults tofalse, is carried onto the client when set, and stays set alongsideenableFullAiCapture: true.withPrivacyModeunit tests inpackages/ai/tests/utils.test.tscovering the per-call value and both client-level spellings.pnpm --filter posthog-node test:unit— 1114 passed.pnpm --filter @posthog/ai test:unit— 1129 passed, 1 skipped.check-typesclean for both packages.Follow-up work
Not in scope here, from the same matrix note / neighbouring rows: the
@posthog/aiprovider tests still assert this path by monkey-patching a mock client, so they could be moved onto a real client now that the option exists.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Fully autonomous
Opened by the scheduled SDK-compliance agent, which reads the compliance matrices in PostHog/sdk-specs and implements one backwards-compatible gap per run. This run's candidate was posthog-node × Capture AI, picked over runner-ups (posthog-node HTTP Client flags-retry backoff timing; posthog-python
$release_idon minimized$feature_flag_called) because it is a user-facing privacy guarantee that is silently inert today, and the remediation is self-contained.Tools: Claude Code (Read/Edit/Bash, vitest, oxlint, tsc). The one judgement call:
withPrivacyModenow checks bothprivacyModeandprivacy_moderather than renaming the client field to snake_case — keeping the camelCasePostHogOptionsname on the client matches the surrounding code, and keeping the old spelling avoids breaking anyone who sets it directly.Agent-authored; requires human review.
Created with PostHog Desktop
🤖 Generated with Claude Code