# Analytics

<!-- docs-test setup: owner -->
<!-- docs-test dns: published -->

Every figure in the dashboard comes from one query catalogue, and the API exposes it as it is: you name a **metric
set**, a **dimension** and a few numbers, and UsherStats writes the query. There is no SQL to send and nothing to
escape. The dashboard reads through these same routes.

All of them need the `analytics:read` scope. A site in another workspace, or outside a token's site restriction, is
`404`.

| Route | Scope |
|---|---|
| `GET /v1/stats/catalogue` | `analytics:read` |
| `GET /v1/sites/{id}/stats` | `analytics:read` |
| `POST /v1/sites/{id}/stats/query` | `analytics:read` |
| `POST /v1/sites/{id}/stats/batch` | `analytics:read` |
| `GET /v1/sites/{id}/summary` | `analytics:read` |
| `GET /v1/sites/{id}/top/{dimension}` | `analytics:read` |
| `POST /v1/sites/{id}/internal-link` | `sites:write` |

The examples assume `$US_TOKEN` holds a token with `account:admin`. First, a site to read:

```sh
curl https://api.usherstats.com/v1/sites \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Field notes", "hostnames": ["fieldnotes.example"]}'
```
<!-- expect 201; SITE_ID=.site.id -->

## The catalogue

Four metric sets: `pageviews` (confirmed pageviews in a browser), `sessions`, `requests` (everything your server or
proxy answered, crawlers included) and `events`. Each lists its lanes (`human`, `bot`, `internal`), the dimensions
it can be broken down by, the filters it takes and the counts it can sort by.

```sh
curl https://api.usherstats.com/v1/stats/catalogue \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

## One query

A spec is `metrics` and `by`, plus any of:

- `lane`: `human` (People), `bot` (Crawlers) or `internal` (your own team). Each metric set has a default.
- `days` (calendar days ending today; `1` is today so far, default `7`), or `from` and `to` (YYYY-MM-DD).
- `compare`: `true` adds `previous`, the period before (a single day is compared with the same weekday a week
  earlier), or says why it cannot be compared yet.
- `where`: filters, such as `{"source": "google"}`. In a query string, `where.source=google`.
- `sort`: which count a table is ranked by, such as `sessions` or `pageviews`.

Analytics Engine keeps 90 days; past that, UsherStats answers from the daily rollups each site keeps for as long as
the account exists. Hourly series and page-to-page pairs are not rolled up, so they answer only within 90 days.

```sh
curl "https://api.usherstats.com/v1/sites/$SITE_ID/stats?metrics=pageviews&by=source&days=30&sort=sessions" \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

The same as JSON (the form an agent's tool call takes):

```sh
curl https://api.usherstats.com/v1/sites/$SITE_ID/stats/query \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"metrics": "requests", "by": "bot", "lane": "bot", "days": 7, "compare": true}'
```
<!-- expect 200 -->

A name that is not on the catalogue's lists is `400`, and `field` says which:

```sh
curl "https://api.usherstats.com/v1/sites/$SITE_ID/stats?metrics=pageviews&by=blob3" \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 400 -->

## Many queries at once

Up to 40 named specs in one call. A spec that is refused answers `{"error": {"status": 400, "title": ...}}` under
its name; the others still answer.

```sh
curl https://api.usherstats.com/v1/sites/$SITE_ID/stats/batch \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"queries": {"byDay": {"metrics": "pageviews", "by": "day", "days": 30}, "crawlers": {"metrics": "requests", "by": "category"}}}'
```
<!-- expect 200 -->

## The People summary

The People boxes as numbers: sessions, pageviews, from search, average session, bounce rate, single-page visits and
pages per session, each with the previous period's figure and the change. A change carries a `tone`: `good` or `bad`
where one direction is better (a lower bounce rate is better), `flat` when nothing moved.

```sh
curl "https://api.usherstats.com/v1/sites/$SITE_ID/summary?days=7" \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

## Top tables

`source`, `medium`, `referrer`, `campaign`, `page`, `page_type`, `country` and `device` count People sessions (where
each began) or, with `count=pageviews`, pageviews. `bot`, `category`, `kind`, `network`, `path`, `status`, `cache` and
`decision` count requests; `event` counts events.

```sh
curl "https://api.usherstats.com/v1/sites/$SITE_ID/top/page?days=30&count=pageviews&limit=20" \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

```sh
curl "https://api.usherstats.com/v1/sites/$SITE_ID/top/bot?days=7" \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

## Marking your own browser

Your team's visits belong in Internal, not People. Tools mark themselves with a signed header; a browser is marked
by opening a link on your site's own first-party path (`/_us/`, which the Worker SDK and the first-party proxy set
up). The link works for ten minutes, once per browser you open it in, and the mark lasts 90 days. `POST /v1/sites/{id}/internal-link/rotate` replaces the site's internal key, so every
browser marked before stops counting as yours (mark them again).

The link is made only for a hostname the site has verified (its TXT record, see [sites](sites.md)) or serves in proxy
mode: it sets a cookie on that domain. Verify it first:

```sh
curl -X POST https://api.usherstats.com/v1/sites/$SITE_ID/hostnames/fieldnotes.example/verify \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

```sh
curl https://api.usherstats.com/v1/sites/$SITE_ID/internal-link \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "fieldnotes.example"}'
```
<!-- expect 201 -->

See also [goals and experiments](goals.md), [bot protection](bot.md), [search](search.md) and
[export](export.md).
