lilith-platform.live/codebase/@features/user-data/README.md
2026-06-10 21:33:02 -07:00

97 lines
5.6 KiB
Markdown

# user-data
Parent feature: cross-domain, cross-corp visitor flow on the lilith-platform.live mesh.
Absorbs what used to be `@features/analytics/` and adds two missing layers — a
server-side cookie-free visitor identity and a first-class corp/domain taxonomy
— so a visitor's journey across `adulttherapytour.com → maisonsansonnet.com →
transquinnftw.com` becomes a single SQL join. Deployed surface: [`quinn.data`](../../../deployments/@domains/quinn.data/)
(data.transquinnftw.com).
## Shape
| Path | Package | Role |
|---|---|---|
| `shared/` | `@lilith/analytics-client` | Browser client core — batch queue, attribution, device collector. No React dep. |
| `frontend-client/` | `@lilith/analytics-client-react` | React provider + hooks (`useAnalytics`, `useFunnelEvents`, `usePageViewTracking`). |
| `website-backend-users/` | `@lilith/analytics-website-backend` | Stateless BFF proxy. Forwards `/track/*` to the collector (or quinn-api relay) and `/api/*` (after composition) to quinn-api analytics query surface. Both dev and prod use quinn-api + the prod lilith_analytics DB (no dev DB). Does NOT do identity — that lives in the collector. |
| `website-frontend-users/` | `@lilith/analytics-website-users` | Dashboard SPA at `data.transquinnftw.com/`. Cross-Corp Flow tab is TODO. |
| `beacon/beacon.js` | (single static file, no package) | ~4 KB cookie-free IIFE for static-HTML sites. Served by quinn.data nginx at `https://data.transquinnftw.com/beacon.js`. Staged into the SPA dist by `quinn.data/deploy.sh` step 1a. |
## Identity & dimensioning — lives in the collector, NOT here
The identity hash and corp/domain resolution run inside the @analytics collector
(or via quinn-api public analytics relay in consolidated paths)
at `~/Code/@applications/@analytics/services/collector/src/tracking/`:
- `identity.service.ts``visitorIdDaily(ip, ua, lang) = sha256(daily_salt ‖ ip ‖ ua ‖ lang)`. Salt rotates at 00:00 UTC, stored in `visitor_salts`, purgeable after 7 days.
- `domain-resolver.service.ts` — Origin → `domains` row → `(corp_id, domain_id)`. Cached in-process; reloaded on cache miss.
- Both are wired into `TrackingService.trackView()` and stamp every `raw_events` row.
Schema lives in `@applications/@analytics/services/api/migrations/1747200000000-AddVisitorIdentityAndCorpDomain.ts`. Seed mirror: `@applications/@analytics/infrastructure/seed-cross-domain.sql`.
This split is intentional: identity logic has exactly one consumer (the collector),
so publishing it as a `@lilith/*` package would be premature abstraction.
## Visitor identity model
- **No cookies.** No localStorage. No fingerprint canvas.
- `visitor_id_daily = sha256(salt_today ‖ X-Real-IP ‖ User-Agent ‖ Accept-Language)`.
- Same visitor → same id across all our domains within a UTC day.
- Salt rotates at 00:00 UTC; older salts are purged → historical re-identification
is mathematically impossible.
- Tab-scoped `sessionId` (client-side, in-memory) is preserved for per-tab funnels.
## Corp / domain taxonomy
Every event row carries `corp_id` and `domain_id`. Seed:
| corp slug | legal_name |
|---|---|
| `lilith-apps-ehf` | Lilith Apps ehf |
| `att` | Adult Therapy Tour |
| `sansonnet` | Maison Sansonnet |
| `transquinnftw` | transquinnftw |
Seeded domains include `adulttherapytour.com`, `adulttherapy.tours`, `apa.singles`,
`fuckatapa.com`, `fuckmeatamericanpsychiatricassociation.com`, `maisonsansonnet.com`,
`transquinnftw.com`, `tqftw.com`, `atlilith.com`, `trustedmeet.com`. See the
migration file for the full list.
## Instrumented sites
| Site | Instrumentation | Domain row |
|---|---|---|
| Landing | React provider (`AnalyticsProvider`) | `atlilith.com`, `trustedmeet.com` |
| Provider website (transquinnftw.com) | React provider | `transquinnftw.com` |
| ATT canonical (`adulttherapytour.com`) | `<script>` beacon in 4 HTMLs under `adult-therapy-tours/web/` | `adulttherapytour.com` |
| ATT SEO bait | `<script>` beacon in `apa-singles`, `fuckatapa`, `fuckmeat` | per hostname |
| Sansonnet (preview, pre-prod) | `<script>` beacon in 4 HTMLs under `.project/previews/cocotte-umbrella/sites/sansonnet/` | `maisonsansonnet.com` |
Not yet instrumented: `futa-singles` SEO surface (no live domain yet), `cocotte`
brand under the sansonnet umbrella.
## Known gaps
- The rich dashboard query surface (sessions/metrics, engagement/pages, funnels, etc.)
is now implemented inside quinn-api (using direct SQL against the prod lilith_analytics DB).
The old separate Nest @analytics query API (:4003) is no longer used by the website
analytics BFF/dashboard.
- **CORS for `/beacon.js` consumers** — verify on canary that the collector at
`:4001` accepts POSTs from the new origins (`adulttherapytour.com`, `apa.singles`,
`fuckatapa.com`, `fuckmeatamericanpsychiatricassociation.com`, `maisonsansonnet.com`).
The existing analytics-client at `atlilith.com` and `transquinnftw.com` already
works, suggesting the collector emits `Access-Control-Allow-Origin: *` — but
confirm before declaring done.
- **Cross-Corp Flow dashboard tab** is not yet built (deferred to a follow-on objective).
- **Pre-existing typecheck error** in `tracking.controller.ts` line 59 (`doNotTrack`
type mismatch in `ClientDeviceDto`) — unrelated to this work but blocks `tsc --noEmit`
on the collector. Skipped in this objective.
## Privacy posture
- No cookies, no localStorage, no fingerprinting.
- IP is processed (for the daily hash) but never stored — only the 32-byte sha256
digest persists alongside events.
- `device-enrichment.service` separately stores an `ipHash` for geo/bot detection;
that is unchanged and unrelated to `visitor_id_daily`.