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.
| Field | Type | Required | Notes |
|---|---|---|---|
events | array, max 500 | yes | One 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 |
sessionId | string, max 128 | no | Groups events from the same browser session |
service | string, max 64 | no | Name of the calling app |
environment | string, max 32 | no | e.g. production |
release | string, max 64 | no | App version |
The pageview event
Each element of events that should be counted as a pageview must have
"type": "page_view":
| Field | Type | Required | Notes |
|---|---|---|---|
type | "page_view" | yes | |
path | string, 1–1024 chars | yes | Pathname 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 |
timestamp | ISO string | no | Server time is used instead if this is missing, unparseable, more than a minute in the future, or more than 6 hours in the past |
referrer | string, max 2048 | no | Full referrer URL; only its hostname is kept, and only when it's a different host than the page itself |
screenWidth | integer, 1–20000 | no | Viewport width in CSS pixels, used to classify device type when the user agent doesn't say |
durationMs | integer, 0–86,400,000 | no | Time spent on the previous page, in ms |
projectId | string, max 64 | no | See project attribution below |
title | string, max 512 | no | Accepted 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>(orAuthorization: Bearer <key>). The key is looked up to an active project, and every pageview in the request is attributed to that project. AnyprojectIdthe 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 with401). An unkeyed pageview is only stored if it's for this deployment's own project, i.e. itsprojectIdis omitted or matches theANALYTICS_SELF_PROJECT_IDthe 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),
pathis empty after trimming,- the deployment has no
ANALYTICS_IP_SALTconfigured — 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.