Docs

Install the script

One tag in the head of your pages. Everything below this is either verification or one of the four cases that need more than a paste.

Verified against the shipping code, September 2026

Add your site

Create the site in your dashboard first. Register the bare hostname — no scheme, no path, no trailing slash, and no www. unless your visitors genuinely see www. in the address bar. The hostname you register here is the one that goes in the tag, and the two have to agree.

A subdomain is a different site. blog.example.com and example.com are counted separately unless you deliberately give both tags the same data-domain, which is a legitimate thing to do when you want one set of numbers across both.

Paste the script tag

One tag, in <head>, on every page you want counted.

The whole installation
<script defer data-domain="example.com" src="https://cdn.absolutelyanalytics.com/aa.js"></script>

defer is what you want: the script is fetched in parallel with parsing and runs once the document is ready, so it never blocks rendering. async works too, and so does injecting the tag from a tag manager.

If you intend to send events from your own code before the script has finished downloading, add the two-line loader stub above it. It queues calls and replays them the moment the real script arrives, so nothing fired during page load is lost.

With the loader stub, for code that calls the API early
<script>window.aa=window.aa||function(){(window.aa.q=window.aa.q||[]).push(arguments)}</script>
<script defer data-domain="example.com" src="https://cdn.absolutelyanalytics.com/aa.js"></script>

Every attribute the tag understands

Attribute Default What it changes
data-domain the page's own hostname Which site the events belong to. Set it explicitly; the default is a convenience for one-page tests, not for production.
data-api derived from the script's own URL Where events are posted. Set it when you serve the script from your own domain — see proxying.
data-auto on data-auto="false" stops all automatic pageviews, including History API navigations and back/forward. You then send every pageview yourself.
data-hash on, in the hash build only Counts a change of URL fragment as a navigation. Needs the hash build; data-hash="false" turns it off again.
data-local off data-local="true" stops local addresses being ignored. For testing only.
data-exclude none Comma-separated path patterns that are never counted. Needs the exclusions build — see excluding traffic.
data-include none Count only paths matching these patterns. Same build. data-exclude wins where both match.
data-exclude-search off data-exclude-search="true" records the path only. The query string and the fragment are dropped before the event is sent.
data-revenue-currency none Fallback ISO 4217 currency for revenue events that do not name one — see revenue tracking.

Values are compared exactly. data-local="1", data-auto="0" and a bare data-local with no value all do nothing at all — the strings the script looks for are precisely "true" and "false".

Choosing a build

The base aa.js gives you pageviews, automatic single-page-app navigation, custom events sent from your own code, and the visitor opt-out. Optional behaviour is compiled into separate builds so that a site that only wants outbound links does not download a form parser it will never run.

Ask for the features you want by naming them in the filename, joined by dots, always in this order:

Feature Adds
hashCounts fragment changes as navigations
outbound-linksAn event on every click to another host
file-downloadsAn event on every click to a file
tagged-eventsEvents declared in your HTML, with no JavaScript
revenueAmounts and currencies on conversions
exclusionsdata-exclude and data-include
engagementScroll depth and engaged time
manualAutomatic pageviews compiled out entirely
Outbound links and downloads, on a hash-routed app
<script defer data-domain="example.com"
  src="https://cdn.absolutelyanalytics.com/aa.hash.outbound-links.file-downloads.js"></script>

There is exactly one correct spelling per combination, because the order is fixed. Naming the same two features the other way round is a URL that does not exist, not an alias — and a script tag pointing at a missing file fails silently, which is the worst failure mode there is. Copy the order from the table above.

Deploy, then verify

Three ways to prove it works, in increasing order of how much they tell you.

  1. Open the realtime view. Load your page in another tab. You should appear within seconds. If you are on localhost, you will not — see the first question below.
  2. Watch the network tab. Open your browser's developer tools, filter to the request the script makes on page load, and read what it sends. This is also the fastest way to satisfy yourself about the privacy claims: there is no cookie header, no identifier, and nothing resembling a fingerprint in it.
  3. Use the verify button in your dashboard. It fetches the page from the public internet, follows redirects, finds the tag, checks the domain matches the site, and reads your Content Security Policy to tell you which directive is blocking it. It reports what it actually saw, which is a five-second diagnosis instead of a support ticket.

Nothing you do in your own browser is stored against you: the check for whether this browser should be counted is made before the first request is sent, not after it arrives.

Handle your framework

Single-page apps

If your router navigates with the History API — which React Router, Vue Router, SvelteKit and Next.js all do for a real page change — you do not have to do anything. A navigation fires a pageview, and so does the browser's back and forward button. So does a page restored from the back/forward cache, which most analytics scripts miss entirely.

Three behaviours worth knowing before you debug something that is working correctly:

  • A navigation that changes only the query string is not a new pageview. Filter and sort states on the same page do not inflate your numbers.
  • A router that replaces the history entry rather than pushing one does not fire a pageview. That is the shallow-routing pattern some frameworks use for state that is not a real page change; if yours uses it for genuine navigations, send the pageview yourself.
  • A change of URL fragment is not a navigation either, unless you are on the hash build.
Sending a pageview yourself, wherever your router announces a route change
window.aa('pageview');

Use data-auto="false", or the manual build, if you want to take over pageviews completely. Note that this switches off everything automatic, including back/forward and back/forward-cache restores — you are then responsible for all of them.

Hash routing

Apps that route on the fragment — /#/pricing — need the hash build. With it, a fragment change is a navigation, the fragment is part of the recorded page, and repeat visits to the same path with different fragments are counted separately.

A hash-routed app
<script defer data-domain="example.com" src="https://cdn.absolutelyanalytics.com/aa.hash.js"></script>

Do not combine the hash build with data-exclude-search="true". The second one strips the fragment from the recorded URL, so you would be counting fragment changes as navigations and then throwing away the fragment that distinguishes them.

Serving the script from your own domain

You can host the script and the collection endpoint on your own origin and forward requests to us. People do this to survive content blockers, to satisfy a strict script-src 'self' policy, or because their security review will not approve a third-party host.

You build and run that forwarding yourself — there is no proxy product to buy here, and we would rather say so than let you plan around one. What the script gives you is the data-api attribute, which points it at your endpoint instead of ours.

A self-hosted proxy install
<script defer data-domain="example.com"
  data-api="https://example.com/aa/event"
  src="https://example.com/aa/js/aa.js"></script>

The verify check in your dashboard understands this shape and will not report a proxied install as a missing one.

What to do next

Pageviews, referrers, campaigns, countries, devices and realtime visitors all work now with no further configuration. The two things most people add next are custom events and goals, so that conversions show up alongside traffic, and excluding your own visits, so that your team's browsing stops counting as traffic.

Common questions

Why am I not seeing anything on localhost?

Because local traffic is dropped before anything is sent, and that is the default on purpose — the alternative is that every developer on your team quietly pollutes production numbers.

The script ignores the file: protocol, localhost, 127.0.0.1, [::1], an empty hostname, and any hostname ending in .local. Add data-local="true" to the tag to switch that off while you are testing, and take it out again afterwards.

Does the script slow my site down?

It is 1270 bytes gzipped, loaded with defer, and it never blocks rendering. It also never calls preventDefault() on a click, so a tracked link navigates at exactly the speed it would have without us — beacons are sent with a keepalive request that survives the page going away, rather than by delaying the navigation to squeeze a request in.

Can I load it through Google Tag Manager?

Yes. Paste the tag as a Custom HTML tag firing on all pages. The script finds its own configuration by looking at the tag it was loaded from, and falls back to searching the document for a script carrying data-domain, so an injected tag is found the same way an inline one is.

Worth knowing before you do: a tag manager is itself a large third-party script, so loading a 1 KB analytics script through it is a strange trade. If the tag manager is only there for us, put the tag in your HTML instead.

My Content Security Policy blocks it. What do I allow?

Two directives. script-src has to allow the host you load the script from, and connect-src has to allow the host it posts events to. If you proxy both through your own domain, 'self' covers you and no third-party host needs allowing at all.

The verify check in your dashboard parses your policy and tells you which directive is missing, rather than leaving you to read a console error.

Do I need to change anything to be GDPR compliant?

Nothing about the install. No cookie is set, nothing identifying is written to localStorage, and no IP address is stored, so there is no consent to collect for this script. You still have your own obligations as the data controller — see privacy and compliance.