Spoar

Concepts

Projects, keys, events, pageviews, routes, visitors, sessions, consent and bot scores.

Projects

A project is one site or app: an id such as example.com, a domain, the origins allowed to send events, a retention period in days and a visibility. Projects are public by default, which means anyone can read their aggregates. A private project answers only to its members and tokens, and answers 404 to everyone else so its name does not leak.

Keys and tokens

CredentialPrefixUsed byCan
Public keypk_The browser SDKSend events, only from the project's allowed origins
Secret keysk_The server SDK and the proxySend events from anywhere, with the visitor's IP and user agent forwarded
API tokenat_Scripts, CI and other frontendsRead, administer or run SQL, depending on its scope

Secret keys and API tokens are shown once, when created or rotated, and stored hashed.

Events

Every event has a name and optional flat props. List your custom events once in a type, and the browser and server clients check every call against it:

import type { NoProps } from "@spoar/sdk";

export type Events = {
  signup: { plan: "free" | "pro" };
  checkout: { revenue: number; currency: string };
  newsletter_subscribed: NoProps;
};
analytics.track("signup", { plan: "pro" });
analytics.track("newsletter_subscribed");

track("signup") without plan is a type error. A name is any string up to 64 characters; the built-in names below are snake_case, and custom names read best in the same style.

NameSent by
pageviewThe core, <Analytics /> or analytics.page()
identifyanalytics.identify()
click, outbound_click, file_download, form_submitThe clicks, outboundLinks and forms plugins
scroll_depth, engagementThe scrollDepth and engagement plugins
web_vitalThe speedInsights plugin
errorcaptureError, captureMessage, the errors plugin and ErrorBoundary
not_found, experiment_exposureThe notFound and experiments plugins
page_requestcreatePageCounter in server middleware

Pageviews and routes

A pageview records the path, the page title and, on the first pageview of a page load, the referrer. The core sends one on load and on every client-side navigation: pushState, replaceState to a new path or query string, back and forward. Hash routers (/#/pricing) report the path inside the hash. A change of only an anchor (#comments) is not a new pageview.

A route is the template behind a path: /blog/[slug] for /blog/hello and /blog/world. Reports can group by route, so every blog post adds up to one row. <Analytics /> from /next sends routes automatically; with other routers, useRoutePageviews and computeRoute from /react, or analytics.route("/blog/[slug]"), set it. Without a route, reports group by path.

Visitors and sessions

A visitor is a random id in localStorage, one per browser and site. A session is a random id in sessionStorage that ends after 30 minutes without events, and at the latest when the tab closes. No cookies are set. analytics.reset() starts a new visitor and session, which is what you call on logout.

analytics.identify(userId, traits) links the visitor to your own user id, so one person on several devices can be seen as one user. Like every event it follows the consent rules below, and consent.revoke() forgets it.

Events sent from the server use server as visitor and session, unless the call passes the browser's ids. Reads count those events in pageviews, events and breakdowns, but never as a visitor or a session.

consent optionBefore a choiceAfter consent.grant()After consent.revoke()
"optional" (default)Events are sentEvents are sentQueued events are dropped, the identity is forgotten, nothing is sent
"required"Events are held in memoryHeld events and new ones are sentHeld and queued events are dropped, the identity is forgotten, nothing is sent

The choice is remembered across page loads and applies to the site's other open tabs at once. Separately, analytics.optOut() stops all sending from this browser until optIn(), and a browser with Do Not Track or Global Privacy Control on sends nothing.

Your own traffic

  • Opening any tracked page with ?ra=ignore (with the ignoreSelf plugin) stops that browser from sending anything; ?ra=track undoes it.
  • Events sent while you are signed in to the dashboard as an owner or admin are stored as internal, and reports leave them out. The API sees the session cookie only on events that come through the proxy or the server client; a browser sending to the API directly carries no cookie.
  • Events from localhost and preview deployments are marked as well.

Bot scores

Every event gets a score from 0 (human) to 100 (bot), built from the user agent, the network, the request headers, the optional botSignals plugin and, later, the session's behaviour. Events are stored whatever their score, with the reasons, and reports count only events that score under 50. A rescore after tuning the weights changes past reports too.

On this page