
# Analytics

Sovrium ships a built-in, first-party analytics engine. It tracks page views, sessions, referrers, UTM campaigns, and device breakdowns on a single unified event model — with no cookies, no fingerprinting, and no external services. All data stays on your server, which makes it GDPR-friendly by default.

Analytics is configured via the `analytics` property in the app schema. When enabled, Sovrium injects a lightweight (~1KB) tracking script that records page views via `navigator.sendBeacon()` to `POST /api/analytics/collect`.

## Configuration

`analytics` accepts a boolean shorthand or an object for fine-grained control.

```yaml
# Boolean shorthand — enable with all defaults
analytics: true

# Object form — override individual settings, keep defaults for the rest
analytics:
  enabled: true
  retentionDays: 365
  excludedPaths:
    - /admin/*
    - /api/*
  respectDoNotTrack: true
  sessionTimeout: 30
```

| Property            | Default | Range / Values      | Description                                                         |
| ------------------- | ------- | ------------------- | ------------------------------------------------------------------- |
| `enabled`           | `true`  | boolean             | Enable/disable analytics tracking.                                  |
| `retentionDays`     | `365`   | `1`–`730`           | Days to retain analytics data. Older rows are automatically purged. |
| `excludedPaths`     | `[]`    | string[] (glob)     | URL patterns excluded from tracking (e.g. `/admin/*`, `/api/*`).    |
| `respectDoNotTrack` | `true`  | boolean             | Honor the browser `DNT:1` header — those visitors are not tracked.  |
| `sessionTimeout`    | `30`    | `1`–`120` (minutes) | Idle period after which a new session begins.                       |

:::callout
**Boolean vs object.** `analytics: true` enables defaults (365-day retention, DNT respected, 30-minute sessions). Use the object form to override specific settings while keeping defaults for the rest. `analytics: false` (or omitting the property) disables analytics — no endpoints are mounted.
:::

## Unified Events Model

All analytics events — page views, custom track calls, and future sources — share a single `system.analytics_events` table with an `event_type` discriminator column. This follows the industry-standard pattern used by PostHog, Mixpanel, and Amplitude, but self-hosted with no external dependency (see DEC-019).

```
system.analytics_events
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid()
  app_name        TEXT NOT NULL
  event_type      TEXT NOT NULL          -- 'page_view' | 'track' | extensible
  event_name      TEXT NOT NULL          -- path for page_view, custom name for track
  org_id          TEXT                   -- organization scope (nullable pre-auth)
  timestamp       TIMESTAMPTZ NOT NULL DEFAULT now()
  visitor_hash    TEXT NOT NULL          -- privacy-preserving visitor identifier
  session_hash    TEXT NOT NULL          -- privacy-preserving session identifier
  properties      JSONB NOT NULL DEFAULT '{}'
```

| Event type  | `event_name`          | `properties` payload                                                                                |
| ----------- | --------------------- | --------------------------------------------------------------------------------------------------- |
| `page_view` | The page path         | `path`, `referrer`, `utm_*`, `device_type`, `browser_*`, `os_*`, `screen_*`, `language`, `country`. |
| `track`     | The custom event name | Arbitrary key-value metadata from an automation action or the tracking API.                         |

The promoted columns (`event_type`, `event_name`, `org_id`, `visitor_hash`, `session_hash`, `timestamp`) drive efficient `WHERE`/`GROUP BY` queries; everything else lives in JSONB. The same privacy primitives — server-side visitor/session hashing (SHA-256, no PII stored), DNT respect, and the retention horizon — apply uniformly to every event type, and every row is org-scoped for multi-tenant isolation.

:::callout
**Storage-engine parity.** The unified events table is exercised by both the PostgreSQL (`->>` over `system.analytics_events`) and [SQLite](/en/docs/database-infrastructure) (`json_extract` over `system_analytics_events`) query paths. Both produce identical response shapes for collect, overview time-series, top-pages, referrer/UTM, and device breakdown.
:::

## Querying Analytics

Aggregated analytics are exposed through admin-gated read endpoints. Inspect raw events for debugging or custom dashboards.

| Endpoint                       | Returns                                                      |
| ------------------------------ | ------------------------------------------------------------ |
| `POST /api/analytics/collect`  | Public ingest endpoint hit by the tracking script.           |
| `GET /api/analytics/overview`  | Totals + bucketed daily time series.                         |
| `GET /api/analytics/pages`     | Top pages by view count.                                     |
| `GET /api/analytics/referrers` | Referrer and UTM-campaign breakdown.                         |
| `GET /api/analytics/devices`   | Device-type and browser breakdown.                           |
| `GET /api/analytics/campaigns` | UTM campaign analysis.                                       |
| `GET /api/analytics/events`    | **Admin-only** raw event inspection (paginated, filterable). |

### Inspecting raw events

`GET /api/analytics/events` provides row-level visibility into the unified stream — complementing the aggregated endpoints. It is admin-only and supports filtering by `event_type`, `event_name`, and date range.

```
GET /api/analytics/events?event_type=track&limit=50&offset=0&from=2026-01-01&to=2026-04-15
```

| Parameter     | Description                               |
| ------------- | ----------------------------------------- |
| `event_type`  | Filter by type (`page_view`, `track`, …). |
| `event_name`  | Exact-match event name.                   |
| `from` / `to` | ISO 8601 date range (inclusive).          |
| `limit`       | Max results (default `50`, max `1000`).   |
| `offset`      | Pagination offset (default `0`).          |

## Privacy & Compliance

- **No cookies, no fingerprinting.** Visitors are identified by a server-computed SHA-256 hash; no client-side identifier is stored.
- **DNT respected by default.** Visitors sending `DNT:1` are not tracked when `respectDoNotTrack` is on.
- **All data is local.** No third-party services and no external calls — analytics never leaves your server.
- **Bounded retention.** Data older than `retentionDays` is automatically purged, aligning with the platform's data-minimisation posture (see [Ecoconception](/en/docs/ecoconception)).

## Related Pages

- [Admin Dashboard](/en/docs/admin-dashboard) — operator read API across the platform.
- [Activity Monitoring](/en/docs/activity-monitoring) — audit trail of user and admin actions.
- [Automations](/en/docs/automations-overview) — emit custom `track` events from automation actions.
- [Ecoconception](/en/docs/ecoconception) — data retention and environmental footprint.
- [Database Infrastructure](/en/docs/database-infrastructure) — the SQLite/PostgreSQL parity behind the events table.
