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.
Install
Section titled “Install”<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.
Runtime Data Flow
Section titled “Runtime Data Flow”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 tofetch(..., { 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.
Retry Behavior
Section titled “Retry Behavior”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.
Flush Triggers
Section titled “Flush Triggers”HitKeep flushes the in-memory queue when:
- the retry timer fires after a short backoff
- the browser fires
online - the page receives
pagehide document.visibilityStatechanges tohidden
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.
Duplicate Pageview Suppression
Section titled “Duplicate Pageview Suppression”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.
Double-Bootstrap Guard
Section titled “Double-Bootstrap Guard”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.
Attribution and Storage Boundaries
Section titled “Attribution and Storage Boundaries”HitKeep separates session continuity from attribution:
| Data | Where it lives in the browser | Purpose |
|---|---|---|
| Opaque session ID | sessionStorage under hk_session | Group pageviews in the same active browser session |
| Session timestamp | sessionStorage under hk_session | Expire inactive sessions |
| Page ID | JS memory | Link automatic events to the current pageview |
| Initial referrer | JS memory | Preserve the first referrer during the current page runtime |
| Initial UTM values | JS memory | Preserve campaign attribution during the current page runtime |
| Retry queue | JS memory | Retry transient delivery failures while the tab is alive |
| Dedupe state | JS memory | Suppress 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.
SPA Tracking
Section titled “SPA Tracking”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.
Cloud Signup Tracking
Section titled “Cloud Signup Tracking”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.
Options
Section titled “Options”| Attribute | Effect |
|---|---|
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.