← all branches

feat/ph-consent

needs attentionviewing older commit
bf92308 · incrementalpre-PRreviewed 2026-07-31 23:56 UTC2H · 8M · 8L
The branch
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)
The changes (whole branch)
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+53428apps/web+16314
Blast
7 files, +471/−18 incremental; packages/analytics library boundary + apps/web public site; no impact on apps/platform or domain logic
compliance LFPDPPP analytics consent
typechecktests· new test file added; not run in this reviewci· no PR; no CI datacoderabbit· no .coderabbit.yaml

Findings · 18

correctness3

medium

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.

low

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.

low

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

high

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.

high

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.

medium

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.

low

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

medium

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.

medium

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.

medium

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.

low

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.

low

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

medium

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.

medium

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.

low

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

medium

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.

medium

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.

low

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

  1. 8c748c7safeincremental0H · 0M · 2L2026-08-01 00:40
  2. 22fc175safeincremental0H · 0M · 4L2026-08-01 00:30
  3. f26dee1needs attentionincremental2H · 2M · 9L2026-08-01 00:24
  4. bf92308needs attentionincremental2H · 8M · 8L2026-07-31 23:56current
  5. cf3e330needs attentionincremental0H · 6M · 5L2026-07-31 23:17
  6. 5fb4d78blockedfull4H · 8M · 9L2026-07-31 22:11