Install the tracker
One script tag. No build step, no dependencies, no cookies, and roughly 9 KB over the wire. Everything below is optional configuration on top of it.
The snippet
<script async src="https://hldgroup.org/analytics/hlda.js"
data-site="hlda_site_xxxxxxxxxxxxxxxx"></script>Put it in <head>. It is async, so it never blocks rendering, and every listener it attaches is passive — it cannot slow a scroll even if it wanted to. Your site key comes from the Website Analytics surface in the HLD admin console.
active. Data collection starts on the first pageview after that, and does not backfill.If you use a queue
Calls made before the script finishes loading are not lost if you stub the queue first. This is worth doing when you call identify() during page render.
<script>
window.hlda = window.hlda || function () {
(window.hlda.q = window.hlda.q || []).push(arguments)
}
</script>
<script async src="https://hldgroup.org/analytics/hlda.js"
data-site="hlda_site_xxxxxxxxxxxxxxxx"></script>Options
Every collector is on by default. Set an attribute to false to turn one off. Turning something off in the snippet stops it being collected at all — this is not a display filter.
| Name | Type | Required | Description |
|---|---|---|---|
| data-site | string | Yes | Your site key. Public by design. |
| data-endpoint | string | No | Override the ingest URL. Defaults to the origin the script was served from. |
| data-clicks | boolean | No | Click, rage-click, dead-click, outbound and download tracking. Default true. |
| data-pointer | boolean | No | Pointer attention sampling for the heatmap. Default true. |
| data-scroll | boolean | No | Scroll depth milestones and per-page reach. Default true. |
| data-forms | boolean | No | Form start, field focus, submit and abandonment. Never values. Default true. |
| data-vitals | boolean | No | Core Web Vitals (LCP, CLS, INP, TTFB). Default true. |
| data-errors | boolean | No | Uncaught errors and unhandled rejections. Default true. |
| data-spa | boolean | No | Watch history for route changes. Default true; turn it off only for a genuinely static site. |
| data-sample | number | No | 0–1. Sampled per visitor, not per page — sampling per page would halve sessions and make every funnel wrong. Default 1. |
| data-flush | number | No | Milliseconds between batches. Minimum 2000, default 5000. |
| data-ignore | string | No | Comma-separated path prefixes that are never recorded — an app behind a login, an admin console. Checked on every event, so a client-side navigation into an excluded section stops recording at the boundary rather than at the next reload. A prefix matches the section, not a name that merely starts with it: /admin excludes /admin/users and leaves /administration alone. |
| data-dnt | boolean | No | Honour the browser’s Do Not Track signal. Default true — a visitor sending DNT is not tracked at all, and nothing is written to their storage. Set false only with a reason you can defend. |
Single-page applications
With data-spa left on, the tracker wraps history.pushState and replaceState and listens for popstate, so a framework that swaps the view without a navigation still reports a pageview per route. A router that changes the URL some other way can announce it directly:
// After your router has committed the new route
hlda('pageview')Identify a visitor
Identity is never inferred. It appears on a session only when your own site says who someone is — after a form submit, or inside a signed-in area. Call it once you know.
hlda('identify', {
email: 'jordan@example.com.au',
name: 'Jordan Reid',
company: 'Example Pty Ltd'
})analytics:visitors key can read.Custom events and conversions
// A named event with optional detail. Scalars only; nested
// objects are dropped rather than walked.
hlda('track', 'quote_calculator_used', { plan: 'business' })
// A conversion, with a value if you have one. Flushed immediately
// rather than waiting for the next batch.
hlda('conversion', 'enquiry_submitted', 2400, { source: 'pricing_page' })// On-site search. The term is only ever what you pass.
hlda('search', 'incident response', { results: 12 })
// Player milestones: 'play', 'progress' or 'complete'.
hlda('video', 'progress', { id: 'sentinel-overview', seconds: 45 })Conversions can also be declared without any code: set conversion paths on the site (for example /thank-you) and reaching one counts. Use the API call when the conversion has a value, or when it does not correspond to a URL.
Content Security Policy
If your site sends a CSP, the tracker needs two allowances — one to load, one to send.
script-src https://hldgroup.org;
connect-src https://hldgroup.org;Checking it works
- Load a page and look for a
POSTto/api/web-analytics/collectin the network tab. A204is success. - The endpoint answers
204for rejected payloads too — deliberately, so it cannot be used to probe what gets through. If nothing appears in the dashboard within a minute, check the site key and that the site's status isactive. - Sessions from cloud and scanner networks are flagged as bots and hidden by default. If your own test visit is missing, tick Include flagged bots — a VPN is the usual reason.