# Tracking & Stats

## Impression tracking

Every served ad counts as one impression and is stored in the `HourlyStats` model.
The model aggregates impressions and clicks **hourly** per ad.

| Field (HourlyStats) | Type | Description |
| --- | --- | --- |
| `ad` | FK → Ad | Reference to the served ad. |
| `hour` | DateTime | Hour (rounded to the full hour) in which the events occurred. |
| `impressions` | Integer | Number of impressions in this hour. |
| `clicks` | Integer | Total clicks in this hour (all clicks, including repeat clicks). |
| `unique_clicks` | Integer | Unique clicks – only counted when the visitor has a valid consent cookie and has not clicked this ad before. |

> Hourly granularity enables detailed intraday analysis with minimal database growth (max. 24 entries per ad per day).

## Click tracking

Clicks are captured differently depending on the ad type:

### Image Ad

The image is wrapped in an `<a>` tag.
The link points to the tracking endpoint `/go/cl/{ad_id}/`,
which logs the click and then redirects to the click URL.

### HTML Ad

All `href="https://…"` links in the HTML content
are rewritten server-side to pass through the tracking endpoint before redirecting.
Additional clicks on the container (no link) also trigger the tracking endpoint.

## Consent cookie (seco\_)

The `seco_` cookie is an **opt-in only** cookie
– it is only set when the visitor has explicitly accepted tracking via your consent banner.
Without this cookie, every click counts as a regular click (no unique tracking, no frequency caps, no deduplication).

| Cookie attribute | Value | Description |
| --- | --- | --- |
| Name | `seco_` | Single cookie for all zones. Contains a UUID session identifier. |
| Lifetime | `6 months` | Persists until the visitor withdraws consent or the browser clears cookies. |
| Path | `/go/` | Scoped to the ad-serving path only. |
| SameSite | `None` (with `Secure`; `Lax` on plain http) | Sent on browser navigations (clicks) even from other origins. Not sent on subresource requests. |
| HttpOnly | `Yes` | Not accessible via JavaScript. Contains a UUID, no PII. |

> For details on embedding the consent snippets in your cookie banner, see [Cookie Consent Integration](/docs/consent).

## Deduplication

When a visitor has a valid `seco_` cookie, the server checks Redis
to determine whether that session has already clicked the same ad. The Redis key pattern:

`click:{session_id}:{ad_id} → TTL: 6 months (matches cookie lifetime)`

The first click sets the key and increments `unique_clicks`. Subsequent clicks increment
only `clicks`. Without a cookie, only `clicks` is incremented (no deduplication).

## Targeting statistics

The `HourlyTargetingStats` model
stores impressions split by targeting dimensions:

| Dimension | Possible values | Source |
| --- | --- | --- |
| `device_type` | desktop, mobile, tablet, bot | Parsed from the User-Agent header |
| `country` | ISO 3166-1 Alpha-2 (e.g. DE, AT, CH) | Via GeoIP lookup (MaxMind) |
| `hour` | DateTime | Hourly aggregation like HourlyStats |

> Targeting stats are visualised in the dashboard device chart and available via CSV export (targeting report).

## CTR calculation

The Click-Through Rate (CTR) shows what fraction of impressions led to a click:

CTR (%) = (clicks ÷ impressions) × 100

**Example: 150 clicks out of 10,000 impressions**

(150 ÷ 10,000) × 100 = 1.50 %

When impressions = 0, CTR = 0 %. The system avoids division by zero.

## JSON stats API

Campaign statistics can be queried as JSON (login required):

`GET /panel/api/campaign/{id}/stats/?preset=7d`

Parameters:

| Parameter | Values | Default |
| --- | --- | --- |
| `preset` | `7d`, `30d`, `today`, `custom` | `7d` |
| `date_from` | YYYY-MM-DD | Only with preset=custom |
| `date_to` | YYYY-MM-DD | Only with preset=custom |
