← all branches

feat/ph-consent

needs attentionviewing older commit
f26dee1 · incrementalpre-PRreviewed 2026-08-01 00:24 UTC2H · 2M · 9L · 1I
The branch
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
The changes (whole branch)
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+44730apps/web+19310apps/platform+134
Blast
13 files, ~1 062 add / 45 del across packages/analytics (core logic), apps/web (banner + atlas + privacy), apps/platform (dashboard layout)
consent session-replay accessibility privacy-law
ci· pre-PR branch — no CI checkscoderabbit· no .coderabbit.yaml

Findings · 14

correctness3

high

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.

high

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.

low

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

low

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.

low

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.

info

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

low

@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.

low

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.

low

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

medium

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.

low

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

medium

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.

low

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'.

low

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

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