# Analytics and Executive Dashboard

Phase 6 introduces a first-party analytics event stream, governed metric
definitions, daily projections, and permission-scoped executive dashboard cards.
No third-party analytics provider is called in this phase.

## Internal event ingestion

The former frontend-facing ingestion endpoints have been removed. Trusted backend code can still pass events to `AnalyticsEventIngestor`; the internal boundary accepts only:

- a UUID event ID and 16–100 character idempotency key;
- an event name declared in `config/analytics.php`;
- a UTC-compatible occurred timestamp within the configured past/future window;
- explicit `granted`, `denied`, or `unknown` consent;
- optional anonymous/session identifiers; and
- per-event, allow-listed scalar properties of at most 200 characters.

Keys resembling email, phone, name, password, token, secret, address, message,
or content are rejected even if a future allow-list is configured incorrectly.
Event and idempotency identifiers are unique. Repeating the same event returns a
successful replay response; reusing an idempotency key for a different event is
a conflict.

Raw visitor and session identifiers are never stored. With granted consent they
are HMAC-pseudonymized using `ANALYTICS_HASH_KEY`; with denied or unknown consent
the identifiers and properties are discarded and only the minimal event
envelope remains. Analytics events are append-only.

No public HTTP ingestion route is registered.

## Metric governance

Metric definitions live in configuration and are synchronized into versioned
database rows. A definition records its stable code, version, description,
aggregation, numerator event, optional denominator event, precision, and
effective dates. Historical versions are not rewritten when a formula changes.

Phase 6 definitions are:

| Metric | Version | Formula |
|---|---:|---|
| Page views | 1 | count of consent-granted `page.view` events |
| Contact requests | 1 | count of consent-granted `contact.submitted` events |
| Contact conversion rate | 1 | contact submissions / page views × 100 |

All date boundaries are UTC. A zero denominator yields zero, never an undefined
or infinite value. `analytics:aggregate --date=YYYY-MM-DD --days=2` synchronizes
definitions and idempotently refreshes daily projections. The scheduler runs it
hourly at minute 10 for today and yesterday, covering normal late-arriving data.
Operators may rebuild up to 31 consecutive days explicitly.

## Executive read model

The administration overview only queries pre-aggregated daily metrics. Users
need both `dashboard.view` to enter the overview and `analytics.view` to receive
the executive-metric section. Without analytics permission, metric names,
values, definitions, and chart markup are absent from the response.

Cards show the current projected value, definition, a 14-day accessible SVG
trend, and projection freshness. The SVG uses compiled CSS and attributes rather
than inline script or style, preserving the content-security policy.

## Operations

- Set a dedicated, stable `ANALYTICS_HASH_KEY`; rotation breaks pseudonymous
  continuity. It must not be a provider credential.
- Alert when hourly aggregation fails, projections become stale, event rejection
  rates spike, idempotency conflicts rise, or ingestion approaches rate limits.
- Run `analytics:aggregate --date=<date> --days=<1-31>` after correcting a metric
  pipeline issue or importing accepted late events.
- MySQL 8 CI is authoritative for unique constraints and production query plans.
- Phase 13 forwards accepted events through an atomic outbox and consent-gated
  provider port. Raw PII and browser identifiers are not copied into those payloads.
