feat/ph-consent
needs attentionviewing older commitbf92308 · incrementalpre-PRreviewed 2026-07-31 23:56 UTC2H · 8M · 8L- Purpose
- Gate PostHog identity (not just session replay) on user consent to comply with LFPDPPP and match what the consent banner tells visitors
- Goal
- Non-consenting visitors get persistence:memory — no durable cookie or localStorage identity; revocation clears the identity immediately
- Sub-goals
- SG-1: Gate PostHog persistence on consent at init time (analyticsPersistenceFor)
- SG-2: Add resetAnalyticsConsent() for LFPDPPP art. 16 withdrawal path
- SG-3: Add AnalyticsConsentControl widget on privacy page for durable preference management
- SG-4: Test the persistence layer comprehensively (consent-storage.test.ts)
- What
- PostHogProvider now passes persistence:'memory' to posthog.init() when consent is withheld; grantAnalyticsConsent switches to localStorage+cookie; denyAnalyticsConsent and new resetAnalyticsConsent call stopIdentifiableCapture (stopSessionRecording + reset + set_config memory). ConsentBanner copy improved; autoFocus added to Accept button.
- Why
- Previous implementation stopped recording but still wrote a durable PostHog cookie/localStorage key. This silently contradicted the banner's promise of 'no guardamos nada en tu navegador'.
- Areas
- packages/analytics+534−28apps/web+163−14
- Blast
- 7 files, +471/−18 incremental; packages/analytics library boundary + apps/web public site; no impact on apps/platform or domain logic
Findings · 18
correctness3
autoFocus on Accept biases consent toward acceptance
apps/web/src/components/ConsentBanner.tsx:80
autoFocus on 'Aceptar' means a keyboard user's next Space/Enter after the banner appears grants consent without reading it. Focus the banner container or Reject button instead.
Banner flickers on localStorage write failure
packages/analytics/src/providers/session-replay.ts:95
storeConsent notifies listeners after a failed write so the banner closes, but readStoredConsent returns 'unset' next tick and reopens it — visible flash on quota/private-mode failure.
Null activeReplayPolicy defaults requireConsent to false — fails open
packages/analytics/src/providers/PostHogProvider.tsx:197
activeReplayPolicy?.requireConsent ?? false evaluates to false when no sessionReplay prop is passed. Correct today (apps/web passes requireConsent: true), but silently bypasses consent if the prop is ever omitted.
security4
Null activeReplayPolicy → localStorage+cookie persistence without consent
packages/analytics/src/providers/PostHogProvider.tsx:197
If PostHogProvider is mounted without an explicit requireConsent: true policy object, ?? false causes analyticsPersistenceFor to return localStorage+cookie for all visitors regardless of consent — a LFPDPPP compliance risk.
posthog.reset() orphaned cookie/localStorage — verify cleanup
packages/analytics/src/providers/PostHogProvider.tsx:119
Disputed by correctness lens (reset() calls persistence.clear() in the correct order before memory mode). Needs verification against posthog-js internals; if reset() does not clear ph_* cookie/localStorage, add explicit key deletion.
grantAnalyticsConsent does not flush anonymous memory-session events
packages/analytics/src/providers/PostHogProvider.tsx:70
Events from the pre-consent memory session are associated with a transient id that is orphaned when a new durable id is generated on grant. First-session funnel will always appear broken for consenting users.
autoFocus on Accept — LFPDPPP informed-consent risk
apps/web/src/components/ConsentBanner.tsx:80
Auto-focusing the affirmative action is a dark-pattern risk under Mexican privacy law.
tests5
resetStoredConsent with throwing localStorage not tested
packages/analytics/src/providers/__tests__/consent-storage.test.ts:78
storeConsent's QuotaExceededError is tested; resetStoredConsent's equivalent (removeItem throws) is not. Consent revocation is a user-rights flow.
Multi-listener isolation not tested
packages/analytics/src/providers/__tests__/consent-storage.test.ts:56
Only single-listener scenarios exercised. Cannot detect Set-vs-Array dedup bugs or listener leaks.
QuotaExceededError test verifies count but not payload
packages/analytics/src/providers/__tests__/consent-storage.test.ts:63
Should assert the notification argument, not just that it fired once.
Full lifecycle (granted→denied→reset) not covered in one scenario
packages/analytics/src/providers/__tests__/consent-storage.test.ts:85
Composed lifecycle test would catch subscriber-list accumulation bugs.
analyticsPersistenceFor missing (false, granted) test case
packages/analytics/src/providers/__tests__/consent-storage.test.ts:103
A regression returning 'memory' for requireConsent=false+granted would not be caught.
improvement3
analyticsPersistenceFor should accept ReplayPolicy | null
packages/analytics/src/providers/PostHogProvider.tsx:197
Accepting the nullable policy object instead of a de-nulled boolean would make the null case self-documenting and match the shouldStartReplay call pattern.
isReplayAllowedWithConsent receives hardcoded true — dead parameter
packages/analytics/src/providers/session-replay.ts:194
replayGateOpen is always true at the call site in shouldStartReplay. Either inline the guard or remove the parameter.
SSR snapshot is an inline literal instead of shared constant
apps/web/src/app/(marketing)/legal/privacidad/_components/AnalyticsConsentControl.tsx:30
() => 'unset' as const duplicates the SSR default. An exported readStoredConsentOnServer() would unify banner and control.
seo3
metadata.title missing '| Batu' suffix
apps/web/src/app/(marketing)/legal/privacidad/page.tsx:8
Pre-existing: 'Política de Privacidad' should be 'Política de Privacidad | Batu'. Fix in this file touch.
metadata.description below 150-char minimum
apps/web/src/app/(marketing)/legal/privacidad/page.tsx:9
Pre-existing: ~70 chars. Convention requires 150–160 chars in es-MX.
AnalyticsConsentControl h3 — verify heading hierarchy
apps/web/src/app/(marketing)/legal/privacidad/_components/AnalyticsConsentControl.tsx:42
Client-only h3 not visible to crawlers, but verify no heading levels are skipped for assistive tech.
History · 6 commits
- 8c748c7safeincremental0H · 0M · 2L2026-08-01 00:40
- 22fc175safeincremental0H · 0M · 4L2026-08-01 00:30
- f26dee1needs attentionincremental2H · 2M · 9L2026-08-01 00:24
- bf92308needs attentionincremental2H · 8M · 8L2026-07-31 23:56current
- cf3e330needs attentionincremental0H · 6M · 5L2026-07-31 23:17
- 5fb4d78blockedfull4H · 8M · 9L2026-07-31 22:11