Skip to content

chore: INFRA-214 Audit & consolidate app analytics pipelines (PostHog / Supabase / dead custom-API)#131

Merged
MP2EZ merged 8 commits into
developmentfrom
chore/infra-214-audit-analytics-pipelines
Jun 5, 2026
Merged

chore: INFRA-214 Audit & consolidate app analytics pipelines (PostHog / Supabase / dead custom-API)#131
MP2EZ merged 8 commits into
developmentfrom
chore/infra-214-audit-analytics-pipelines

Conversation

@MP2EZ
Copy link
Copy Markdown
Owner

@MP2EZ MP2EZ commented Jun 5, 2026

Closes INFRA-214

Consolidates Being's analytics onto a documented legal-basis routing model and deletes the dead custom-API path.

Routing model (no dual-write): PostHog = consent-gated product analytics; Supabase analytics_events = vital-interest crisis telemetry + operational telemetry.

Tranches:

  • T1 — audit + decision doc (analytics-architecture.md).
  • T2 — deleted the fully-dead custom-API path (AnalyticsService + never-run k-anon/DP engine + api.being.fyi network/cert layer).
  • T3 — crisis-detection event (crisis_detected) routed to Supabase, not PostHog (3-agent decision: PostHog won't init without consent → false all-clear; contradicts privacy policy; device-persistent distinct_id). Durable fire-time enqueue (no silent drop on first-run/offline).
  • T4 — Supabase trackEvent consent gate (cloud_sync) + sanitizer hardening + deleted orphaned product-analytics-to-Supabase emitters.
  • T5 — DPIA v1.2 + lia-crisis-telemetry.md + privacy-policy v1.6 (discloses vital-interest recording without analytics consent).
  • T6 — verifiable durable crisis-landing test.

Net: large dead-code removal. typecheck clean; test:unit 387; test:clinical 94; test:crisis-detection 82; legal-registry consistent; Maestro safety suite 4/4.

k-anon/DP explicitly NOT claimed (never ran). Supabase→PostHog forward deferred (own DPIA). FEAT-129 re-scoped to Supabase. Counsel review of the vital-interests basis due before 500 EU users (DPIA §10).

🤖 Generated with Claude Code

MP2EZ and others added 8 commits June 1, 2026 23:06
… (T1)

Records the three-sink current-state audit (PostHog-direct live; Supabase
analytics_events ops-only with orphaned crisis emitters; custom-API api.being.fyi
fully dead) and the decided target routing model (Option 1: PostHog-direct +
honest DPIA re-scope) in analytics-architecture.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deletes the fully-orphaned custom-API analytics path that never ran:
- AnalyticsService.ts (incl. AnalyticsPrivacyEngine: the never-executed
  k-anonymity / differential-privacy / session-rotation engine and the
  orphaned crisis_intervention_triggered emitter).
- AnalyticsOrchestrator + all unused orchestrator types in analytics/index.ts;
  the barrel now exports only the live PostHog tier (PostHogProvider, PHIFilter,
  useAnalytics, useFeatureFlag, deletion workflow).
- SyncStatusIndicator: drops the "Analytics Service" panel (it read
  AnalyticsService.getStatus() from the dead path and showed hardcoded
  privacyCompliance/networkSecurity — never real signal); now sync-only.
- Deletes 2 dead test files that asserted the never-ran engine
  (analytics-service-integration, week3-analytics-hipaa-compliance). The
  HIPAA-named file tested controls that never executed; Being is not a HIPAA
  entity — removal signed off by the compliance planning pass.

api.being.fyi network-layer remnants (NetworkSecurityService URLs/pins + the
4 unused request wrappers) are inert placeholder config and are removed
separately in T2b. No live control weakened (the engine had zero callers).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the dead-path deletion (AC3). api.being.fyi never resolved (DNS-checked),
had placeholder certs, and NETWORK_CONFIG self-documented the URLs as "placeholders
for future". Removed:
- NETWORK_CONFIG PRODUCTION/STAGING/DEVELOPMENT_API_URL + the api.being.fyi
  CERTIFICATE_PINS placeholder block.
- The four uncalled request wrappers (crisisApiRequest, uploadAssessmentData,
  professionalApiRequest, bulkDataOperation), determineApiBaseUrl + the apiBaseUrl
  field, and validateAPIConnectivity (a swallowed fetch to the dead /health endpoint
  on every crisis-security init).
- API_CERTIFICATE_PINS + its getPinsForHost branch in certificate-pinning.ts.

Kept the generic secureRequest primitive + its helpers (calculateDataHash /
generateRequestId stay used by the kept response-validation path) and ALL Supabase
certificate pinning + CrisisSecurityProtocol init — no live control touched. Updated
the two affected security tests; ratcheted .eslint-baseline.json (-27 violations from
the deletions).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverses the "PostHog = crisis telemetry" part of AC2 after crisis + compliance +
architect planning passes unanimously found PostHog unsuitable for the vital-interests
crisis-detection event:
- PostHog SDK does not init without analytics consent → silent drop on the common
  no-consent path = false "all-clear" safety-monitoring gap.
- Privacy policy promises Being NEVER collects PHQ-9/GAD-7/mental-health data in-app;
  a crisis-detection event is a PHQ/GAD-derived signal → FTC §5 exposure, and PHIFilter
  would itself reject the payload.
- PostHog distinct_id is device-persistent; compliance requires session-rotation, which
  the Supabase analytics_events schema enforces.

New routing rule (legal-basis partition, no dual-write): PostHog = consent-gated product
analytics; Supabase analytics_events = vital-interest crisis telemetry + ops. Two
sanitizers kept intentionally (PHIFilter reject-gate vs Supabase bucket-transform); shared
invariant: no raw PHQ/GAD integer leaves the device. Supabase→PostHog forward deferred;
FEAT-129 queries Supabase for v1. Records the durable-fire-time-enqueue requirement
(else the safety gap is merely relocated to the offline/no-userId path).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the canonical vital-interest crisis-detection event to the Supabase
analytics_events sink (decided in the T3 routing correction; PostHog is unsuitable —
won't init without consent + can't meet session-rotation).

assessmentStore.handleCrisisDetection: after the dedup guard + triggerEmergencyResponse,
fire-and-forget emit of a PII-free, pre-bucketed event (trigger_type, severity_bucket,
intervention_surfaced, assessment_type) — NEVER the raw triggerValue/score. Wrapped so a
telemetry failure can never block or suppress the 988 path.

SupabaseService.trackCrisisDetection + flushCrisisAnalytics + loadCrisisAnalyticsQueue:
a SEPARATE durable queue (own AsyncStorage key, never evicted by the ops queue) persisted
at fire-time, so a first-run/offline crisis is not silently dropped (the gap the architect
flagged). user_id is reconciled at flush time; undeliverable events are retained and
escalated to the local security log. Flush hooks on init + app-foreground.

Test-first: crisisTelemetryEmit.unit.test.ts pins the safety-critical emit contract
(PII-free payload, emit-once-per-episode dedup, fire-and-forget). The deeper durable-queue
reconciliation is covered by the T6 landing test. .eslint-baseline.json accepts the new
test file's typed-parse skip (same systemic condition as sibling src/ test files).

Verified: typecheck clean; test:unit 379, test:clinical 94, test:crisis-detection 82 pass;
the 988 Alert path is unchanged (pre-existing assessmentStore.test failures predate this).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anitizer (T4)

Completes the legal-basis routing model on the Supabase sink:
- Deletes the orphaned useCloudAnalytics hook (trackAssessmentCompletion /
  trackCrisisEvent / trackFeatureUse) and the index.ts trackAnalyticsEvent wrapper.
  These routed PRODUCT analytics to analytics_events — a path that shouldn't exist
  (product analytics → PostHog; crisis → trackCrisisDetection). They were never called.
- Adds a consent gate to SupabaseService.trackEvent: operational telemetry reaches
  analytics_events only with `canPerformOperation('analytics')` (which also honors
  universal opt-out / GPC). Invariant: nothing reaches analytics_events without analytics
  consent EXCEPT the vital-interest crisis-detection event (separate bypass).
- Hardens sanitizeAnalyticsProperties: buckets ANY clinically-named numeric
  (score|result|phq|gad|severity|ideation|suicid), closing the hole where a raw value
  under a key like `phq9_total`/`severity` passed through un-bucketed. Operational
  numerics (size_mb, duration_ms, operation_count, …) are not clinical and pass through.

Note: gate basis (`analytics` vs `cloud_sync`/legitimate-interest for ops telemetry) is
flagged for the T5 compliance pass to confirm.

Tested (analyticsGate.unit.test.ts): consent gate drops without consent, records with;
clinical numeric bucketed while operational numerics pass. typecheck clean; test:unit 382.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(T5/T6)

T5 (compliance docs, drafted by the compliance agent):
- DPIA v1.2: documents crisis-detection telemetry as a new processing activity (§2, §4)
  under GDPR Art. 6(1)(d)/9(2)(c) vital interests, recorded to first-party Supabase
  analytics_events without analytics consent, PII-free/bucketed. Revised §6.1 Scenario 1
  to the two-sink model and to state explicitly that k-anonymity/differential-privacy are
  NOT claimed (the dead engine providing them was deleted in T2). Added §7 controls 13-14
  and a §9 material-change record + v1.2 change-log entry. Founder self-certifies pre-launch.
- New lia-crisis-telemetry.md: standalone vital-interests assessment (necessity, balancing,
  safeguards). Added to the legal-registry internal-only set.
- privacy-policy.md v1.6: §3 + §5.2 disclose that the crisis-detection event is recorded to
  first-party storage WITHOUT analytics consent (the prior "crisis never gated by opt-out"
  text only covered 988 access, not recording). Regenerated legal content.
- T4 gate-basis correction (compliance ruling): SupabaseService.trackEvent now gates ops
  telemetry on `cloud_sync` (not `analytics`) — a cloud-sync user shouldn't lose backup
  telemetry for declining product analytics; still honors universal opt-out.

T6 (verifiable crisis-landing test):
- crisisTelemetryDurable.unit.test.ts pins the durable-queue behavior the architect required:
  a first-run/offline crisis is durably enqueued (no silent drop), reconciles user_id and
  lands in analytics_events, retains on failure, PII-free, fire-and-forget. The migration
  note already lives in analytics-architecture.md (no data migration — pre-launch).

typecheck clean; test:unit 387; test:crisis-detection 82; legal-registry consistent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MP2EZ MP2EZ merged commit 6a6490e into development Jun 5, 2026
22 checks passed
@MP2EZ MP2EZ deleted the chore/infra-214-audit-analytics-pipelines branch June 5, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant