Documentation menu

Behaviour & performance

Cohort retention, navigation paths and Core Web Vitals.

4 endpoints, all relative to https://analytics.appfor.you. Shared rules live in Conventions, Rate limits and Error codes.

Cohort retention

GET/api/v2/sites/{siteId}/retentionscope: read

Computed from session summaries with a 5-minute in-process cache. Retention values are percentages rounded to 1 decimal; index 0 is the cohort period itself and is always 100.

  • This endpoint does NOT accept the usual `range` presets — it is parameterised by `granularity` and `periods` instead.

Path parameters

siteIdstringThe Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.

Query parameters

NameTypeDescription
granularity'day' | 'week' | 'month'Cohort bucket size. Default `week`.
periodsintegerNumber of periods to project, 2–24. Default 8. Alias: `weeks`.
startDatestringISO-8601 or YYYY-MM-DD. Alias: `from`.
endDatestringISO-8601 or YYYY-MM-DD. Alias: `to`.

Example request

curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \
  "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/retention?granularity=week&periods=8"

Example response

{
  "granularity": "week",
  "periods": 8,
  "averageRetention": [100, 41.2, 28.7, 22.1, 18.9, 16.4, 15.0, 13.8],
  "curves": [
    {
      "cohort": "2025-01-06",
      "totalUsers": 512,
      "retention": [100, 44.1, 30.2, 24.0, 20.1, 17.2, 15.4, 14.1]
    }
  ],
  "cohorts": [
    {
      "cohort": "2025-01-06",
      "totalUsers": 512,
      "periods": [100, 44.1, 30.2, 24.0, 20.1, 17.2, 15.4, 14.1]
    }
  ]
}

User journeys

GET/api/v2/sites/{siteId}/journeysscope: read

`topJourneys` groups sessions with more than one pageview by their first 5 pathnames in timestamp order. `entryPages` / `exitPages` are the first and last pathname of each session. `avgPagesPerSession` is the mean pageview count per session, rounded to 1 decimal.

  • `limit` and `offset` apply to `topJourneys`, `entryPages` and `exitPages` alike.

Path parameters

siteIdstringThe Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.

Query parameters

NameTypeDescription
rangestringRelative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.
fromstring (YYYY-MM-DD)Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
tostring (YYYY-MM-DD)Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
limitintegerRows to return. Default 50, minimum 1, maximum 500.
offsetintegerRows to skip, for paging. Default 0.

Example request

curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \
  "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/journeys?range=30d&limit=10"

Example response

{
  "range": {
    "since": "2025-02-19T00:00:00.000Z",
    "until": "2025-03-21T00:00:00.000Z"
  },
  "topJourneys": [
    { "path": ["/", "/pricing", "/signup"], "count": 184 }
  ],
  "entryPages": [
    { "page": "/", "count": 2410 }
  ],
  "exitPages": [
    { "page": "/pricing", "count": 890 }
  ],
  "avgPagesPerSession": 2.4,
  "totalSessions": 5934,
  "limit": 10,
  "offset": 0
}

Core Web Vitals

GET/api/v2/sites/{siteId}/vitalsscope: read

`summary` carries p50/p75/p99 for each metric (null when there are no samples) plus good / needsImprovement / poor percentages for the four core metrics. Thresholds: LCP 2500/4000 ms, FID 100/300 ms, CLS 0.1/0.25, INP 200/500 ms. `trend` is a daily p75 series for LCP, FID and CLS; `byPage` and `byDevice` give p75 for LCP, CLS and INP with sample counts.

  • `limit` / `offset` paginate `byPage` only (sorted by sample count desc). `byDevice` and `trend` are always returned in full.

Path parameters

siteIdstringThe Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.

Query parameters

NameTypeDescription
rangestringRelative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.
fromstring (YYYY-MM-DD)Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
tostring (YYYY-MM-DD)Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
limitintegerRows to return. Default 50, minimum 1, maximum 500.
offsetintegerRows to skip, for paging. Default 0.

Example request

curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \
  "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/vitals?range=30d"

Example response

{
  "range": {
    "since": "2025-02-19T00:00:00.000Z",
    "until": "2025-03-21T00:00:00.000Z"
  },
  "summary": {
    "lcp": { "p50": 1820, "p75": 2410, "p99": 5120, "good": 78.2, "needsImprovement": 16.4, "poor": 5.4 },
    "cls": { "p50": 0.02, "p75": 0.08, "p99": 0.31, "good": 88.1, "needsImprovement": 8.6, "poor": 3.3 },
    "ttfb": { "p50": 210, "p75": 380, "p99": 1290 }
  },
  "trend": [
    { "date": "2025-03-21", "lcp_p75": 2380, "fid_p75": 12, "cls_p75": 0.07 }
  ],
  "byPage": [
    { "pathname": "/", "count": 1820, "lcp_p75": 2210, "cls_p75": 0.05, "inp_p75": 148 }
  ],
  "byDevice": [
    { "device": "desktop", "count": 3120, "lcp_p75": 1980, "cls_p75": 0.04, "inp_p75": 122 }
  ],
  "limit": 50,
  "offset": 0
}

Refused traffic

GET/api/v2/sites/{siteId}/bot-eventsscope: read

Every beacon this product judges a bot is discarded before storage and answered with the same 204 an accepted beacon gets, so this endpoint is the only way an integrator can see what left the dataset. `byReason` carries the machine identifiers (`ua_pattern`, `referrer_spam`, `datacenter_ip`, `js_challenge`, `custom_rule`) each with a `costOfBeingWrong` line naming who a false positive on it actually is -- that is the difference between "we blocked 43 bots" and "we deleted 12 of your customers". `byPath` says which of the three ingestion routes refused it (`collect`, `vitals`, `recordings`); rows written before the path was recorded report `unknown` rather than being attributed to a guess. `sample` is the most recent rows with the hashed address, so the counts can be checked against real user agents rather than trusted. The rule itself is documented in `docs/bot-rule.md`.

  • `sessions` is the number of DISTINCT VISITS behind the refusals, counted once across every reason. `byReason[].sessions` is a per-reason set and the sets overlap, so adding them up over-counts -- use the top-level figure.
  • `datacenter_ip` is off by default. A site only produces it when its owner sets `treatDatacenterAsBot` to `"corroborated"` (refuse when the client also fails to identify a browser engine) or `true` (refuse on the address alone).
  • `ip` is a truncated SHA-256 of the client address, never the address. It is stable within a site, so repeat offenders group, and it does not reverse to an IP.
  • `retention.actual` is `indefinite` and says so deliberately: `BotEvent` declares a 30-day TTL index that has never been built, and `Site.dataRetentionDays` is validated, plan-capped, displayed and enforced by nothing. Do not build a compliance report on the declared figure.
  • `limit` / `offset` paginate `sample` only. `byReason`, `byPath` and `byDay` are always returned in full.

Path parameters

siteIdstringThe Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere.

Query parameters

NameTypeDescription
rangestringRelative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied.
fromstring (YYYY-MM-DD)Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
tostring (YYYY-MM-DD)Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400.
limitintegerRows to return. Default 50, minimum 1, maximum 500.
offsetintegerRows to skip, for paging. Default 0.

Example request

curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \
  "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/bot-events?range=30d&limit=5"

Example response

{
  "range": {
    "since": "2025-02-19T00:00:00.000Z",
    "until": "2025-03-21T00:00:00.000Z"
  },
  "total": 2351,
  "sessions": 1804,
  "addresses": 972,
  "byReason": [
    {
      "reason": "ua_pattern",
      "count": 2351,
      "sessions": 1804,
      "addresses": 972,
      "costOfBeingWrong": "a browser whose user agent matched a bot pattern. Cheap to get wrong: the patterns match self-identifying crawlers and headless automation, which no ordinary visitor sends."
    }
  ],
  "byPath": [
    { "path": "collect", "count": 2301 },
    { "path": "vitals", "count": 44 },
    { "path": "unknown", "count": 6 }
  ],
  "byDay": [
    { "date": "2025-03-21", "count": 84 }
  ],
  "sample": [
    {
      "reason": "ua_pattern",
      "path": "collect",
      "sessionId": "s_8f2a1c",
      "ip": "9d2f1a4b8c3e5077",
      "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/146.0.0.0 Safari/537.36",
      "referrer": null,
      "timestamp": "2025-03-21T09:14:02.118Z"
    }
  ],
  "retention": {
    "declared": "30 days, by the TTL index on BotEvent.timestamp",
    "actual": "indefinite",
    "note": "The TTL index is declared and has never been built."
  },
  "limit": 5,
  "offset": 0
}