feat/ph-consent
needs attentionviewing older commitf26dee1 · incrementalpre-PRreviewed 2026-08-01 00:24 UTC2H · 2M · 9L · 1I- Purpose
- Gate PostHog session replay strictly to routes where it is intentional, and make the consent banner less intrusive. Previously replay started at PostHogProvider init, which meant the recorder could run on any page in both apps.
- Goal
- Route-scope session replay via a mountable SessionReplayScope component; ship a discreet corner-card ConsentBanner that does not steal focus
- Sub-goals
- SG-1: Introduce SessionReplayScope — a client component that starts/stops recording based on consent + env + policy, with cleanup on unmount
- SG-2: Remove replay from PostHogProvider init; init always sets disable_session_recording=true
- SG-3: Mount SessionReplayScope only on Atlas (web) and the authenticated dashboard (platform)
- SG-4: Redesign ConsentBanner to a discreet bottom-left corner card with role=region
- SG-5: Update privacy policy to reflect scoped recording and accurate tool language
- What
- New SessionReplayScope component that is mounted only in opted-in routes; PostHogProvider init no longer starts replay; grantAnalyticsConsent no longer starts recording directly; ConsentBanner redesigned to a compact corner card; privacy policy text condensed and corrected
- Why
- Recording the entire apps (both web and platform) on every page was both over-broad and a privacy risk. Route-scoping gives precise control without changing the consent model.
- Areas
- packages/analytics+447−30apps/web+193−10apps/platform+13−4
- Blast
- 13 files, ~1 062 add / 45 del across packages/analytics (core logic), apps/web (banner + atlas + privacy), apps/platform (dashboard layout)
Findings · 14
correctness3
useEffect silently skips recording for returning consented visitors if PostHog loads after mount
packages/analytics/src/providers/SessionReplayScope.tsx:40
Effect deps are [consent] only. A returning visitor with stored consent='granted' has stable consent — if posthog.__loaded is false on first run, the effect early-exits with no cleanup registered, then never re-runs. Fix: subscribe to posthog's loaded callback or add a loaded-state dependency.
getActiveReplayPolicy() returns null if PostHogProvider's useEffect hasn't run yet
packages/analytics/src/providers/SessionReplayScope.tsx:43
React doesn't guarantee sibling useEffect order. If SessionReplayScope fires before PostHogProvider sets activeReplayPolicy, shouldStartReplay returns false. For apps/platform (requireConsent=false), consent never changes, so recording never starts. Same for web returning users with stored consent.
sessionReplay prop JSDoc implies init-time behavior that no longer applies
packages/analytics/src/providers/PostHogProvider.tsx:162
The prop now only feeds activeReplayPolicy for SessionReplayScope. Passing sessionReplay=undefined while mounting a SessionReplayScope causes shouldStartReplay to return false silently. Interface documentation needs updating.
security3
Consent banner may not meet LFPDPPP 'expreso' requirement — keyboard users cannot reach it
apps/web/src/components/ConsentBanner.tsx:45
autoFocus removed + role='region' (not announced on appearance). A keyboard user tabbing through the page may never discover the banner. LFPDPPP requires consent to be 'libre, inequívoco, específico, informado y expreso'. Consider aria-live='polite' or restoring focus management.
Privacy disclosure omits that autocapture events fire pre-consent
apps/web/src/app/(marketing)/legal/privacidad/page.tsx:170
PostHog autocapture (clicks, page views, element text, full URLs) fires for all visitors including those who deny or never answer. The notice says 'sólo contamos visitas de forma anónima' but doesn't disclose click/URL event collection. Accurate but incomplete under LFPDPPP art. 15.
Sampling draw not cleared on denyAnalyticsConsent — minor inconsistency
packages/analytics/src/providers/session-replay.ts
resetStoredConsent clears the sessionStorage draw key; denyAnalyticsConsent does not. A user who accepts, gets sampled in, then denies and re-accepts will re-use the cached draw. Harmless but inconsistent with the revocation semantics.
conventions3
@remarks JSDoc on grantAnalyticsConsent documents branch history — belongs in PR
packages/analytics/src/providers/PostHogProvider.tsx:73
References 'an earlier revision on this branch' and 'no caller outside this repo existed' — task history, not invariant documentation. Delete; move to PR description.
SessionReplayScope JSDoc enumerates current mount sites — caller context that will rot
packages/analytics/src/providers/SessionReplayScope.tsx:15
Lists 'apps/web … Atlas only, apps/platform … The authenticated dashboard only'. Caller context ('used by X') explicitly forbidden by project conventions. Keep the rule (route-scoped, not app-scoped); move the mount-site list to the PR description.
Two separate import statements from 'react' should be merged
packages/analytics/src/providers/SessionReplayScope.tsx:4
import { useEffect } from 'react' and import { useSyncExternalStore } from 'react' on consecutive lines. Merge into one statement.
tests2
SessionReplayScope has no test file — 4 critical behaviors uncovered
packages/analytics/src/providers/SessionReplayScope.tsx
Missing: (1) mount while consent=denied → no recording; (2) mount while consent=granted → recording starts; (3) consent changes to granted while mounted → recording starts without reload; (4) unmount while recording → stopSessionRecording called. Case 4 is the only guard against route-scoped opt-in becoming app-wide.
grantAnalyticsConsent no-longer-starts-recording behavior untested
packages/analytics/src/providers/PostHogProvider.tsx
No regression guard that grantAnalyticsConsent does NOT call startSessionRecording. A future dev re-adding the call breaks route-scoping without a failing test.
improvement3
autoFocus removed from Accept button without replacement accessibility announcement
apps/web/src/components/ConsentBanner.tsx:75
Old code had explicit rationale: 'the banner appears after hydration, so without it a keyboard or screen-reader user has no idea it arrived.' New design drops autoFocus with no aria-live, no aria-atomic. A role='region' is not announced on appearance. Add aria-live='polite' to the container, or restore autoFocus.
Redundant explicit 'return undefined' at end of useEffect
packages/analytics/src/providers/SessionReplayScope.tsx:50
React treats an effect returning undefined identically to returning nothing. Remove the trailing 'return undefined'.
Two consecutive JSDoc blocks — first is silently discarded by TypeScript tooling
packages/analytics/src/providers/PostHogProvider.tsx:65
TypeScript associates only the LAST /** */ block before a declaration. The first (describing the function's purpose) is orphaned by the second (@remarks). Merge into one block.
History · 6 commits
- 8c748c7safeincremental0H · 0M · 2L2026-08-01 00:40
- 22fc175safeincremental0H · 0M · 4L2026-08-01 00:30
- f26dee1needs attentionincremental2H · 2M · 9L2026-08-01 00:24current
- bf92308needs attentionincremental2H · 8M · 8L2026-07-31 23:56
- cf3e330needs attentionincremental0H · 6M · 5L2026-07-31 23:17
- 5fb4d78blockedfull4H · 8M · 9L2026-07-31 22:11