Skip to content
Start in Cloud

Tracker Architecture

The browser tracker has one job: send useful analytics to your HitKeep instance without turning the visitor’s browser into a long-term identity store.

hk.js is served by your HitKeep instance, posts only to your HitKeep instance, and does not set analytics cookies. The only browser storage used by the public tracker is sessionStorage for the existing opaque session tuple: a random session ID and its last activity timestamp.

<script async src="https://your-hitkeep.example/hk.js"></script>

That default snippet records pageviews, SPA route changes, and the built-in automatic events described in Automatic Events: outbound_click, file_download, and form_submit.

Web Vitals collection is off by default. Add data-enable-web-vitals="true" when you want HitKeep to load the same-origin hk-vitals.js split bundle and send LCP, INP, CLS, FCP, and TTFB samples to /ingest/web-vitals.

If HitKeep is mounted under a path prefix, load the tracker from that mounted path:

<script async src="https://www.example.net/hitkeep/hk.js"></script>

The tracker derives its endpoints from the hk.js script URL. In the example above, pageviews post to /hitkeep/ingest, automatic events post to /hitkeep/ingest/event, and opt-in Web Vitals load /hitkeep/hk-vitals.js before posting to /hitkeep/ingest/web-vitals. Do not hard-code separate ingest URLs in the snippet.

sequenceDiagram
    participant Page as Visitor page
    participant Tracker as hk.js
    participant Memory as JS memory
    participant Storage as sessionStorage
    participant Ingest as HitKeep /ingest
    participant Queue as Embedded NSQ
    participant DB as DuckDB data plane

    Page->>Tracker: Load script
    Tracker->>Storage: Read or create hk_session
    Tracker->>Memory: Keep page ID, initial referrer, initial UTM values
    Tracker->>Tracker: Build pageview payload
    Tracker->>Ingest: sendBeacon(payload)
    alt Browser accepts beacon
        Ingest->>Queue: Enqueue hit
        Queue->>DB: Batch flush
    else sendBeacon returns false or is unavailable
        Tracker->>Ingest: fetch(payload, keepalive, credentials omitted)
        Ingest->>Queue: Enqueue hit
        Queue->>DB: Batch flush
    end

Automatic events follow the same delivery path through /ingest/event. The tracker stores privacy-safe event properties only, strips query strings and hashes, and does not read link text, form field values, or request bodies.

Opt-in Web Vitals use /ingest/web-vitals. The default hk.js bundle does not include the Web Vitals library; it loads hk-vitals.js only when the snippet contains data-enable-web-vitals="true". Web Vitals payloads include metric name, numeric value, normalized path, navigation type, session ID, page ID, tracker source, and tracker version. They do not include attribution payloads, selectors, text, resource URLs, query strings, or hashes.

The delivery path intentionally has two layers:

  • Browser delivery: sendBeacon() first, with a boolean fallback to fetch(..., { keepalive: true, credentials: "omit" }).
  • Server delivery: the ingest handler validates and queues the hit into embedded NSQ before DuckDB writes happen in batches.

sendBeacon() is best suited for analytics during unload, but it can return false when the browser cannot queue the payload. HitKeep treats that return value as a delivery failure and immediately falls back to keepalive fetch.

For backend forwarding, CMS plugins, and historical replay jobs, use Server-Side Tracking. Server-side tracking uses API client authentication and caller-provided RFC3339 timestamps.

For AI visibility, keep the two signals separate. hk.js captures AI-referred human visits when a person arrives from an assistant. It does not capture AI crawler fetches reliably because most crawlers do not run JavaScript. Forward crawler requests through AI fetch ingest when you need the AI Visibility dashboard.

The tracker keeps retries in memory only. It does not write failed hits to cookies, localStorage, sessionStorage, IndexedDB, or another persistent client store.

flowchart TD
    A["Build pageview or event"] --> B{"Can send now?"}
    B -->|"sendBeacon true"| C["Done"]
    B -->|"sendBeacon false"| D["Try fetch with keepalive"]
    B -->|"sendBeacon disabled or unavailable"| D
    D -->|"HTTP 2xx"| C
    D -->|"Network error or non-2xx"| E["Append to in-memory retry queue"]
    E --> F{"Queue over 10 items?"}
    F -->|"Yes"| G["Drop oldest pending hit"]
    F -->|"No"| H["Keep pending hit"]
    G --> H
    H --> I["Flush on retry timer, online, pagehide, or hidden visibilitychange"]
    I --> D

The queue is bounded to 10 pending payloads. If the tab closes before a failed payload can be retried, that payload is lost. That is deliberate: durable client-side retry queues improve delivery, but they also increase the amount of analytics state stored on the visitor’s device.

HitKeep flushes the in-memory queue when:

  • the retry timer fires after a short backoff
  • the browser fires online
  • the page receives pagehide
  • document.visibilityState changes to hidden

visibilitychange to hidden is usually the last reliable moment to send analytics before a tab is backgrounded or closed. pagehide is used as a companion signal for navigation and bfcache-friendly unload behavior.

Some applications bootstrap scripts twice or emit the same route transition more than once during hydration. HitKeep suppresses duplicate pageviews for the same path inside a short in-memory window.

flowchart LR
    Route["Pageview candidate"] --> Same{"Same path as last pageview?"}
    Same -->|"No"| Send["Send pageview"]
    Same -->|"Yes"| Window{"Inside duplicate window?"}
    Window -->|"Yes"| Suppress["Suppress duplicate"]
    Window -->|"No"| Send

The suppression state is kept only in the tracker closure. Reloading the page clears it.

hk.js also guards against double bootstrap through window.hk._bootstrapped. If the same snippet is injected twice, only the first instance registers listeners and sends the initial pageview.

This is especially important in dashboards, CMS templates, and tag-manager setups where partial page rendering can accidentally include the same script more than once.

HitKeep separates session continuity from attribution:

DataWhere it lives in the browserPurpose
Opaque session IDsessionStorage under hk_sessionGroup pageviews in the same active browser session
Session timestampsessionStorage under hk_sessionExpire inactive sessions
Page IDJS memoryLink automatic events to the current pageview
Initial referrerJS memoryPreserve the first referrer during the current page runtime
Initial UTM valuesJS memoryPreserve campaign attribution during the current page runtime
Retry queueJS memoryRetry transient delivery failures while the tab is alive
Dedupe stateJS memorySuppress duplicate pageviews for the same path

The tracker does not use analytics cookies, localStorage, IndexedDB, or a sessionStorage queue. UTM values and referrer attribution are not stored long-term in the browser by the tracker.

Automatic event listeners run in memory only. Disable individual classes with data-disable-outbound-tracking, data-disable-download-tracking, or data-disable-form-tracking when a site needs a narrower tracking surface.

By default, hk.js patches history.pushState, history.replaceState, and listens for popstate so route changes in single-page applications record pageviews.

Disable SPA route tracking when you only want the first page load counted:

<script
async
src="https://your-hitkeep.example/hk.js"
data-disable-spa-tracking="true"
></script>

That option is useful for narrowly scoped tracking, such as measuring unauthenticated traffic to a hosted signup page without following logged-in dashboard navigation.

HitKeep Cloud instances use the same normal hk.js script for their public signup page. The cloud signup page injects the regional script only when:

  • the instance is running as hosted cloud
  • cloud signup is enabled
  • the current visitor is not logged in

The injected script disables SPA and automatic event tracking:

<script
async
src="/hk.js"
data-disable-spa-tracking="true"
data-disable-outbound-tracking="true"
data-disable-download-tracking="true"
data-disable-form-tracking="true"
></script>

That records only the signup pageview for guest traffic on the specific cloud instance. It does not track logged-in dashboard users.

AttributeEffect
data-disable-spa-tracking="true"Do not patch browser history or track SPA route changes
data-disable-outbound-tracking="true"Disable automatic outbound link events
data-disable-download-tracking="true"Disable automatic file download events
data-disable-form-tracking="true"Disable automatic form submission events
data-enable-web-vitals="true"Load same-origin hk-vitals.js and send opt-in Web Vitals samples
data-disable-beacon="true"Skip navigator.sendBeacon() and use fetch delivery directly
data-collect-dnt="true"Override Do Not Track and collect even when DNT: 1 is present

For the strongest privacy posture, leave data-collect-dnt unset so HitKeep respects Do Not Track by default.