# Analytics

PRD §29 / MP §13.3. **Components never talk to a provider.**

```
Blade: data-track="cta_click" data-track-props='{"cta":"hero_view_work","location":"hero"}'
   └─▶ resources/js/analytics/listen.js   (one delegated click listener, page_view, 'portfolio:track' events)
         └─▶ resources/js/analytics/track.js   track(event, props): the ONLY provider call site
               └─▶ adapter: null | plausible | umami | ga4   (from <meta name="analytics">, i.e. Admin › Analytics)

Server: CvController / ContactController ─▶ AnalyticsTracker (null | database | log | plausible | ga4)
```

## Swap or add a provider

- **Swap:** Admin › Analytics. Choose the in-browser provider and server driver, then enter the site ID (Plausible domain, Umami website ID, or GA4 measurement ID). No code or deploy needed. The CSP picks up the provider's origin automatically. GA4 shows a consent banner first; Plausible and Umami are cookieless and need none.
- **Add a new client provider:** add a case to `AnalyticsClientDriver`, a default script URL in `AnalyticsManager::DEFAULT_SCRIPTS`, and an adapter in `track.js` (`adapters.myprovider = async (config) => ({ send(event, props) {…} })`).
- **Add a new server provider:** handle it in `SendAnalyticsEvent::handle()` and `AnalyticsManager::tracker()`.

## Add an event

1. Add a case to `App\Domain\Analytics\AnalyticsEvent`.
2. Run `php artisan portfolio:analytics-catalogue` (regenerates `resources/js/analytics/events.generated.js`; a test fails if you forget).
3. Fire it with `data-track="your_event"` in Blade, or `track('your_event', {...})` / `window.dispatchEvent(new CustomEvent('portfolio:track', { detail: { event, props } }))` in a module.

Unknown event names are ignored, with a console warning in development, and a test checks that every `data-track` in templates is in the catalogue.

## Event catalogue (26)

| Event | Fired from | Props |
|---|---|---|
| `page_view` | every page | `path` |
| `cv_download_{header,hero,experience,contact,footer,mobile_bar,nav,block}` | CV buttons (client) | `location` |
| `cv_view`, `cv_download` | `/cv`, `/cv/download` (server, first-party) | `location` |
| `cv_unavailable_click` | CV fallback buttons | `location` |
| `contact_submit_attempt`, `contact_submit_success` | contact form (client + server) | `location` |
| `linkedin_click`, `social_click` | social and share links | `platform`/`network`, `location` |
| `project_card_click` | project cards and experience links | `project`, `source` (homepage, listing, related, featured, grid, experience) |
| `case_study_view`, `case_study_scroll_depth`, `case_study_time_on_page` | project detail (`case-study` module) | `project`, `depth` 25/50/75/100, `bucket` |
| `external_link_click` | credential links, case-study links, recommendation sources | `type` |
| `cta_click` | buttons and nav links | `cta`, `location` |
| `booking_cta_click`, `booking_widget_open` | booking buttons; embed loads and meeting types | `location`, `meeting` |
| `recommendation_carousel_advance`, `recommendation_interaction` | carousel | `position` |

**Privacy:** no names, emails, IPs or message content are ever sent. First-party server events store only event, location, path and time (`analytics_events`, pruned after `ANALYTICS_RETENTION_DAYS`).
