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

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

Note:Nothing is recorded until the snippet is on the page and the site is 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.

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

NameTypeRequiredDescription
data-sitestringYesYour site key. Public by design.
data-endpointstringNoOverride the ingest URL. Defaults to the origin the script was served from.
data-clicksbooleanNoClick, rage-click, dead-click, outbound and download tracking. Default true.
data-pointerbooleanNoPointer attention sampling for the heatmap. Default true.
data-scrollbooleanNoScroll depth milestones and per-page reach. Default true.
data-formsbooleanNoForm start, field focus, submit and abandonment. Never values. Default true.
data-vitalsbooleanNoCore Web Vitals (LCP, CLS, INP, TTFB). Default true.
data-errorsbooleanNoUncaught errors and unhandled rejections. Default true.
data-spabooleanNoWatch history for route changes. Default true; turn it off only for a genuinely static site.
data-samplenumberNo0–1. Sampled per visitor, not per page — sampling per page would halve sessions and make every funnel wrong. Default 1.
data-flushnumberNoMilliseconds between batches. Minimum 2000, default 5000.
data-ignorestringNoComma-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-dntbooleanNoHonour 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:

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

js
hlda('identify', {
  email: 'jordan@example.com.au',
  name: 'Jordan Reid',
  company: 'Example Pty Ltd'
})
Warning:Only call this with details the person gave you for this purpose. It writes to a session record that HLD staff and anyone holding an analytics:visitors key can read.

Custom events and conversions

js
// 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' })
js
// 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.

text
script-src  https://hldgroup.org;
connect-src https://hldgroup.org;

Checking it works

  • Load a page and look for a POST to /api/web-analytics/collect in the network tab. A 204 is success.
  • The endpoint answers 204 for 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 is active.
  • 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.