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.
<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.
<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 |
|---|---|
hash | Counts fragment changes as navigations |
outbound-links | An event on every click to another host |
file-downloads | An event on every click to a file |
tagged-events | Events declared in your HTML, with no JavaScript |
revenue | Amounts and currencies on conversions |
exclusions | data-exclude and data-include |
engagement | Scroll depth and engaged time |
manual | Automatic pageviews compiled out entirely |
<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.
- 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. - 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.
- 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
hashbuild.
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.
<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.
<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.