---
title: "HitKeep JavaScript Tracker Architecture Guide | HitKeep"
description: "Learn how HitKeep hk.js sends pageviews and events, handles sendBeacon fallback, retries in memory, suppresses duplicate pageviews, and stores attribution."
canonical: "https://hitkeep.com/guides/tracking/tracker-architecture/"
---

# HitKeep JavaScript Tracker Architecture Guide

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

```
<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](https://hitkeep.com/guides/tracking/automatic-events/): `outbound_click`, `file_download`, and `form_submit`. For canonical runtime, binary, RAM, storage, privacy, and export facts, see [Facts and Limits](https://hitkeep.com/reference/facts-and-limits/).

Building with React, Vue, Angular, or Astro? The same tracker is published as the typed [`@hitkeep/tracker` npm package](https://hitkeep.com/guides/tracking/npm-package/) for bundler-based frontends.

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.

Teams can also verify [custom tracking domains](https://hitkeep.com/guides/tracking/custom-tracking-domains/). In Site Settings, users can generate a snippet from the instance URL or any active custom tracking domain owned by the site’s team:

```
<script async src="https://analytics.example.com/hk.js"></script>
```

The tracker still derives pageview, event, and Web Vitals endpoints from the script URL. HitKeep serves only tracker assets and ingest routes on custom tracking hosts; dashboard and API routes return `404`.

## 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 to `fetch(..., { keepalive: true, credentials: "omit" })`.
- **Server delivery:** the ingest handler discards browser requests marked as speculative by `Sec-Purpose` or `Purpose` with an empty `202 Accepted`, then validates and queues normal hits 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, reverse proxy forwarding, and historical replay jobs, use [Server-Side Tracking](https://hitkeep.com/guides/tracking/server-side-tracking/). Server-side tracking uses API client authentication and caller-provided RFC3339 timestamps. If Caddy already sees the visitor request, the community [Caddy HitKeep plugin](https://hitkeep.com/guides/tracking/server-side-tracking/#caddy-reverse-proxy-option) can send those pageviews through the same authenticated endpoint.

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](https://hitkeep.com/guides/tracking/ai-fetch-ingest/) when you need the AI Visibility dashboard.

## Speculative Prerendering

Browsers can load a page speculatively before the visitor chooses to open it. HitKeep checks `document.prerendering` and waits for `prerenderingchange` before sending analytics from such a page. The older `visibilityState === "prerender"` signal remains as a compatibility fallback; an ordinary hidden background tab is still tracked immediately.

During prerendering, HitKeep keeps one pending pageview and up to 32 event or Web Vitals payloads in memory. Multiple pageview candidates, including SPA route changes, are coalesced into one pageview for the final activated URL. Campaign attribution, buffered event context, and Web Vitals are resolved against that activated URL. On activation, the pageview is sent first and the buffered events and Web Vitals follow in their original order. If the browser discards the prerender without activating it, nothing is sent.

The browser ingest routes also return an empty `202 Accepted` without storing requests whose `Sec-Purpose` or `Purpose` header contains `prefetch` or `prerender`. This protects deployments that still have an older tracker cached. Authenticated server-side tracking endpoints are not affected by this browser-only safeguard.

## 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

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.

## 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

`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

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 |
| Prerender queue | JS memory | Hold up to 32 events or Web Vitals samples until a speculative page activates |
| 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

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

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

| 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.

## Related

- [NPM Package](https://hitkeep.com/guides/tracking/npm-package/)
- [Automatic Events](https://hitkeep.com/guides/tracking/automatic-events/)
- [Custom Tracking Domains](https://hitkeep.com/guides/tracking/custom-tracking-domains/)
- [Web Vitals Analytics](https://hitkeep.com/guides/analytics/web-vitals/)
- [Server-Side Tracking](https://hitkeep.com/guides/tracking/server-side-tracking/)
- [AI Fetch Ingest](https://hitkeep.com/guides/tracking/ai-fetch-ingest/)
- [AI Fetch on AWS](https://hitkeep.com/guides/tracking/ai-fetch-aws/)
- [AI Visibility Analytics](https://hitkeep.com/guides/analytics/ai-visibility/)
- [UTM Parameters](https://hitkeep.com/guides/tracking/utm-parameters/)
- [Custom Events](https://hitkeep.com/guides/tracking/custom-events/)
- [PECR and ePrivacy](https://hitkeep.com/compliance/pecr-eprivacy/)
- [Architecture](https://hitkeep.com/reference/architecture/)

[Previous Compare alternatives](https://hitkeep.com/vs/)[Next NPM package](https://hitkeep.com/guides/tracking/npm-package/)
