Docs

Cookieless sessions

3x does not need cookies or device storage to count sessions. Before consent the browser keeps its ids in memory only; the server links the requests of one visitor with a hash that changes every day and cannot be recomputed later.

In the browser

  • Every page load creates a random session id (UUID v4) and a random id per pageview. Both live in memory. Navigation inside a single-page app keeps the session id.
  • The session id rotates after 30 minutes without a send.
  • A tab hidden for more than 30 minutes starts a new pageview (nav = resume) and a new session when it becomes visible again. A restore from the back/forward cache starts a new pageview with nav = back_forward.
  • Pageviews are finalized before they turn 24 hours old.
  • After consent the session id is kept in sessionStorage (<prefix>tsid), so a full reload within 30 minutes continues the session in the browser itself.

On the server

Each batch carries the client session id and a flag fresh (generated on this page load). The server resolves the session of every new pageview, oldest first:

  1. Personal pages (for example personalized QR-letter URLs, configured as personal paths) get an isolated session with a random id – no hash, no referrer, no attribution, hour-truncated times.
  2. Alias: a session of the same client session id that is still alive (last seen < 30 min) and has the same campaign → stitch memory (SPA navigation) or storage (restored after consent).
  3. Hash: only when the id is fresh – the latest session with the same visitor hash, alive, started less than a day ago, same campaign → stitch hash.
  4. Otherwise a new session (id = the client id unless taken).

A new campaign starts a new session: when the URL carries attribution (UTM parameters or click ids) that differs on any of the 13 keys from the session's entry attribution.

The visitor hash

visitor_hash = first 32 hex of sha256(daily_salt ‖ ip ‖ "\n" ‖ user_agent ‖ "\n" ‖ project_id)
  • The salt is random, per project and per calendar day in the project time zone. Salts of past days are deleted on every salt access and by the cleanup job, so yesterday's hashes cannot be recomputed – not even by the operator.
  • The scope is the project: all hosts of a project stitch, but a visitor can never be linked across projects or tenants.
  • IP and user agent are used for the hash and for rate limits only and are never stored. The user agent is reduced to device class, browser family and OS family.
  • The hash is computed only for batches that contain a non-personal pageview.

Visitors

Visitors are an estimate: distinct (local day, visitor hash) pairs plus sessions without a hash. A visitor who returns on another day counts again – that is the price of not storing identifiers.

Behind proxies and CDNs

Stitching needs the visitor's real IP. If events reach the API through a proxy that does not forward it, all visitors look alike and are merged. Use the first-party proxy, which forwards the IP with a proxy key, or make sure the API sees the client address (X3_TRUSTED_IP_HEADERS). The dashboard warns when more than 30 % of a day's sessions share one visitor hash.

Stitch kinds in reports

Every session records how it was linked (new, memory, storage, hash) – filter with stitch= or break down by stitch to see how much of your traffic relies on hashing.

Test traffic and opt-out

  • ?x3_test=1 anywhere in a document's lifetime marks all its batches as test traffic. Reports exclude it unless include_test=true.
  • ?x3_notrack=1 stores an opt-out flag in this browser (localStorage, the one write allowed without consent because the user explicitly asked for it); ?x3_notrack=0 removes it. The dashboard's exclude own devices box has both links.
  • Parameter names are configurable per project (testParams, notrackParam).