How to send pageviews to Codmir and what powers the Traffic widget on the project overview.

Web Analytics (Pageviews)

The Traffic widget on a project's overview page is powered by a first-party pageview collector — a small HTTP endpoint that accepts pageview events and stores them against a project, with no cookies and no raw IP address ever persisted.

Quick start

If your app already uses the @codmir/overseer client, pageviews go through the same telemetry client as errors and metrics:

import { telemetry } from "@codmir/overseer/telemetry";

telemetry.init({
  service: "web",
  dsn: "https://api.codmir.com/api/v1/overseer", // default
  apiKey: process.env.NEXT_PUBLIC_OVERSEER_KEY, // ties pageviews to your project
});

telemetry.pageview({
  path: window.location.pathname,
  referrer: document.referrer,
  screenWidth: window.innerWidth,
});

Calling telemetry.pageview() queues the event; the client batches up to 25 events and flushes them every 5 seconds, and again when the page is hidden or navigated away from, so a visitor who leaves quickly is still counted.

Sending pageviews directly

Without the client, POST directly to the ingest endpoint:

POST /api/v1/overseer/ingest
Content-Type: application/json

(application/json or text/plain both work — the endpoint parses the body as JSON either way, which lets navigator.sendBeacon send it as text/plain to avoid a CORS preflight during page teardown.)

Request body

Declared limits (TelemetryIngestBodySchema). The server enforces sessionId, service, environment and release by rejecting a request that exceeds one with 400 Bad Request. events is handled differently: a batch over 500 is truncated to the first 500 rather than rejected outright (see below), since a queued client-side batch can grow past the cap during an error burst and should still have its first 500 events land.

FieldTypeRequiredNotes
eventsarray, max 500yesOne or more event objects; mix pageviews with other telemetry event types freely. A batch longer than 500 is truncated to the first 500 — see below
sessionIdstring, max 128noGroups events from the same browser session
servicestring, max 64noName of the calling app
environmentstring, max 32noe.g. production
releasestring, max 64noApp version

The pageview event

Each element of events that should be counted as a pageview must have "type": "page_view":

FieldTypeRequiredNotes
type"page_view"yes
pathstring, 1–1024 charsyesPathname only — no origin, no query string, no fragment. Query strings and fragments are stripped from anything else sent here, so secrets in a URL (invite tokens, magic links) never reach storage
timestampISO stringnoServer time is used instead if this is missing, unparseable, more than a minute in the future, or more than 6 hours in the past
referrerstring, max 2048noFull referrer URL; only its hostname is kept, and only when it's a different host than the page itself
screenWidthinteger, 1–20000noViewport width in CSS pixels, used to classify device type when the user agent doesn't say
durationMsinteger, 0–86,400,000noTime spent on the previous page, in ms
projectIdstring, max 64noSee project attribution below
titlestring, max 512noAccepted by the schema but not currently read by the collector

A legacy shape is also accepted for compatibility: { "type": "event", "name": "page_view", "path": "..." }.

Example request:

curl -X POST https://api.codmir.com/api/v1/overseer/ingest \
  -H "Content-Type: application/json" \
  -H "x-overseer-key: $OVERSEER_KEY" \
  -d '{
    "events": [
      { "type": "page_view", "path": "/pricing", "referrer": "https://google.com/", "screenWidth": 1440 }
    ]
  }'

Authentication and project attribution

Pageviews are attributed to a project in one of two ways:

  • Keyed — send x-overseer-key: <key> (or Authorization: Bearer <key>). The key is looked up to an active project, and every pageview in the request is attributed to that project. Any projectId the event itself claims is ignored.
  • Unkeyed (open ingest) — with no key, the request is still accepted unless the deployment has set TELEMETRY_OPEN_INGEST=false (which rejects it with 401). An unkeyed pageview is only stored if it's for this deployment's own project, i.e. its projectId is omitted or matches the ANALYTICS_SELF_PROJECT_ID the API is configured with. A pageview claiming any other project is dropped — not stored, not an error — so an anonymous request can't inflate a different project's traffic.

A pageview is also dropped (silently, with no error returned) when:

  • it comes from a bot or script (recognised by user agent, including a missing user agent),
  • path is empty after trimming,
  • the deployment has no ANALYTICS_IP_SALT configured — the visitor identifier is derived from this salt, so without it the collector drops events rather than write rows that can't be told apart.

A single request stores at most 50 pageviews, even if more were sent (the first-party client never approaches this; it exists to bound a forged body).

The response reports how many pageviews from the request were actually stored:

{ "received": true, "processed": 3, "breakdown": { "pageViews": 2, "...": "..." } }

Batches over 500 events

If events has more than 500 elements, the server keeps only the first 500 and processes those — it does not reject the request. The response then includes a dropped count of how many events were cut:

{ "received": true, "processed": 500, "dropped": 7, "breakdown": { "...": "..." } }

dropped is only present when events were actually cut; a batch at or under 500 never includes it. This exists because a client that queues events during an error burst can send a batch past 500 before it ever posts — truncating keeps that incident's first 500 events instead of rejecting the whole batch outright.

An events field that is missing entirely, or an empty array, is accepted with no events processed:

{ "received": true, "processed": 0 }

What the Traffic widget shows

The overview reads back through GET /api/project/:projectId/analytics/traffic?range=1h|24h|7d|30d, which returns:

  • totals — visitors, pageviews, visitors active in the last 5 minutes, and average time on page,
  • delta — percent change against the immediately preceding window of the same length,
  • series — a timeseries of visitors and pageviews, bucketed by the range,
  • pages / referrers — the top paths and referrer hosts by pageviews,
  • geo — country-level visitor counts, for the overview globe.

A project with no pageviews yet reports hasData: false, which is what shows the "Configure the pageview collector" row on the overview instead of an empty widget.