Skip to content
Documentation

One tag, in the head.

The script, its optional attributes, where to paste it on common platforms, and how to see the first event arrive.

The tag

The tracker is one file under 2.5 kB gzipped, identical for every page and every site - the number the landing quotes is the number a visitor downloads. Query exclusions are enforced server-side, so changing them in Settings never requires replacing the tag. Web Vitals ride in the same file and are stored once the performance module is on. It carries defer, so it never blocks the page from rendering, and it sends with sendBeacon, so it never delays navigation either. There is no second file, no dependency, and nothing to initialize.

every page, inside the head
<script defer src="https://stats.kehai.io/k.js?site=YOUR_SITE_ID"></script>
Copy yours from the panel

Copy the tag from Settings → Install. Your site ID is in its URL. Query exclusions are enforced server-side when events arrive, before storage. You do not paste a parameter list or replace the tag after editing it.

It has to load as its own script element, because the tracker reads its site ID and optional attributes from that element before it runs. A tag manager that inserts one is fine. What does not work is copying the code into another script or a bundle, where there is no tag to read.

Attributes

The site ID in the script URL is required. These attributes are optional; query exclusions are managed in Settings.

AttributeWhat it does
data-domainsoptionalComma-separated hosts this tag may report from, which keeps a staging copy out of your numbers. Hosts, never addresses: shop.example.com, not https://shop.example.com and never a port. An entry carrying either matches nothing and the tag goes quiet on every page. Links between listed domains stop counting as outbound.
data-hashoptional"true" counts a change of URL fragment as a page, for applications routed on /#/products. Off by default, because an OAuth flow puts tokens in fragments and a site that does not route on them has nothing to gain. Only a fragment with no = in it ever travels.
data-do-not-trackoptional"true" honors the browser's do-not-track and global privacy signals. Off by default: it is the site owner's choice, stated on the tag.
optional host and privacy controls
<script defer src="https://stats.kehai.io/k.js?site=YOUR_SITE_ID"
        data-domains="shop.example.com,docs.example.com"
        data-do-not-track="true"></script>

Settings follow the tag

The script is the same bytes for every site, so there is nothing per-site to revalidate and no fixed hidden list. A Settings change applies to the next event that arrives - no refresh, no waiting. An empty list in Settings allows all nonempty query parameters within the documented URL limits.

Older /k.js tags with data-site, data-blocked or data-drop still work with their embedded configuration. The short tag needs none of it: intake applies the current site list to every event either way.

Where to paste it

Anywhere inside the head, on every page you want counted. In a few common places:

  • WordPress: install the Kehai Analytics plugin and copy your site ID. A manual snippet in the shared head also works; use one installation method.
  • Webflow: Site settings, Custom code, Head code.
  • Next.js: the root layout's head, or the Script component with the beforeInteractive strategy.
  • Astro, Hugo, plain HTML: the shared head partial, layout, or template every page extends.
  • Google Tag Manager: a Custom HTML tag containing the whole snippet, attributes included, fired on All Pages. It works, with two honest caveats: everything GTM loads waits for GTM, and the container itself is on every content blocker's list, so a tag pasted directly in the head counts visits that a GTM-loaded one will miss. If GTM is where your organization manages tags, use it. If you have the choice, paste directly.

Single page applications need nothing extra: the script follows pushState, replaceState, and the back button on its own.

What will not count, on purpose

  • A local preview of your site sends nothing, on purpose: whoever builds your pages can reload all afternoon without filling your reports. Check the install on the published site, not on a preview.
  • An event from a host your site does not own is refused at intake. Add extra hosts under the Site screen's additional domains, or correct the domain there.
  • A rule you created under Settings, Rules can refuse events at intake. That is your own setting working, and the Install screen names it when it happens.
  • One address sending more than 6,000 events to one site inside a minute is refused for the rest of that minute. This is an abuse limit, and real traffic behind a shared network address can reach it. The Install screen names it when it happens. If it was real traffic, write to hello@kehai.io.
Content security policy

If your site sends a CSP, the collector host needs to be in script-src and in connect-src. With only one of the two, the tag either never loads or never sends, and says nothing either way.

Where a script cannot go

An email, a PDF, a forum profile, a system that strips JavaScript: for those there is an image. It counts a view and nothing else, no engaged time, no vitals, no events, so use it beside the snippet rather than instead of it. Name the page it sits on in u, as a path.

the pixel
<img src="https://stats.kehai.io/px?d=YOUR_SITE_ID&u=/newsletter/october"
     alt="" width="1" height="1" />

Verify it

The Install screen in the panel is the wiring check: it watches the last minutes of traffic, names the hosts events actually came from, and lists anything the collector turned away with the reason and what to change. A site can be receiving and refusing at the same time, which is exactly the case that costs an afternoon, so the screen shows both side by side.