Skip to content

Analytics

CMSKite measures two different things and it is worth knowing which is which. API traffic is what your site asked us for. Analytics is what your readers did. This page is about the second.

Last updated

A request is not a view

The obvious way to count views would be to count requests for a post. We deliberately do not, because it is wrong in four directions at once.

  • A framework fetches at build time. A Next.js site with 200 posts requests every one of them on deploy — 200 views nobody had.
  • A server renders once and serves many. A cached page is fetched from us once and read ten thousand times — one view for ten thousand readers.
  • A list is not a read. Asking for twenty posts does not mean twenty were read.
  • A crawler is not a reader, and it is the thing that most reliably fetches every post exactly once.

So a view is reported by the page that rendered it, at the moment a person saw it, and by nothing else. That is one call from your site.

Two lines to start counting

Examplebash
npm install cmskite
A React or Next.js pagetsx
'use client'

import { useTrackView } from 'cmskite/react'

export function TrackView({ postId }: { postId: string }) {
  useTrackView(postId, { apiKey: process.env.NEXT_PUBLIC_CMSKITE_KEY! })
  return null
}

Render it inside the article. Without a framework, the tracker is a plain object:

Examplets
import { createTracker } from 'cmskite/browser'

const analytics = createTracker({ apiKey: PUBLIC_KEY })

analytics.trackView(post.id)
analytics.trackClick(post.id, 'https://example.com/pricing')

What counts as what

FigureWhat it means
ViewsOne reader seeing one post once, in one day. Five refreshes is one view.
ReadersDaily unique visitors, counted approximately. See below.
ClicksA tracked link pressed. Pressing the same link twice is two clicks.
Click rateClicks per hundred views.

A reader who returns tomorrow is a new view, because a day is the unit anybody reasons in — and because there is deliberately no way to follow somebody across days.

What we collect, which is very little

No cookie is set. Nothing is stored in the browser except a note of which posts this tab has already reported, which is cleared when the tab closes. No IP address and no user agent is ever written down.

A visitor is a hash of the day’s random salt, the project, the address and the browser string. The salt lives in memory for 48 hours and is never written to disk, so once it rotates at midnight the hashes cannot be recomputed — by anybody, including us, from data anybody still holds.

A page path is stored without its query string, because that is where campaign ids and occasionally somebody’s email address end up. A referrer is reduced to its host: news.ycombinator.com, never the full URL.

Why readers is approximate

Counting distinct visitors exactly means keeping the set of visitors, and keeping that set is the thing we have just said we will not do. So readers are counted with a probabilistic counter: 12KB of memory regardless of traffic, accurate to within about 0.8%, and impossible to read back to recover who was in it.

That is the right trade for a blog. “4,182 readers” and “4,150 readers” lead to the same decision, and the difference is not worth a table of every visitor who ever arrived.

It cannot break or slow your site

  • Nothing throws. Every failure inside the tracker is swallowed — your article renders whether or not our analytics is having a good day.
  • Nothing blocks. Events are queued and sent in one batch on a timer, using sendBeacon where it exists, which hands the batch to the browser and returns immediately.
  • Nothing is lost on exit. sendBeacon still delivers after the page has closed, which is when the last view of a reading session is reported.
  • Nothing is double-counted. A refresh, a re-render or a client-side navigation back to a post already read reports nothing at all.

The endpoint answers 202 with an empty body before the event has been stored anywhere. There is nothing in the response to wait for.

Reading the numbers

They appear on the project’s Analytics screen in the dashboard. The same figures are on the API, if you want them in your own tooling:

Examplebash
curl "https://api.cmskite.com/v1/analytics/content/summary?projectId=prj_..." \
  -H "Authorization: Bearer csk_live_..."

curl "https://api.cmskite.com/v1/analytics/content/top-posts?projectId=prj_...&limit=10" \
  -H "Authorization: Bearer csk_live_..."

curl "https://api.cmskite.com/v1/analytics/content/posts/post_...?projectId=prj_..." \
  -H "Authorization: Bearer csk_live_..."

The per-post call answers with more than a series. Alongside the totals it names links — which links readers pressed and how often — plus referrers, the sites they arrived from, and countries where your host supplies one. That is the whole of a post’s Readers panel in the dashboard, in one request.

Every figure comes from a daily rollup, so a project with ten million views costs the same to chart as one with ten. Days are UTC; the dashboard renders them in your timezone. The three breakdowns are the exception: a link target and a referrer host are unbounded, so they are read from the raw events, bounded to one post and one range.

What this does not do

Said plainly so nobody goes looking for it.

  • No funnels, no returning-visitor cohorts, no sessions followed across days. All of those need an identifier that persists, which is the thing we chose not to have.
  • No scroll depth or time on page.
  • No per-visitor drill-down. There is no visitor to drill into.
  • Readers is approximate, by design.
  • Bots are refused when they announce themselves. One that lies is indistinguishable from a reader, and a heuristic that guessed would throw away real views to catch it.